Compare commits

..

1 Commits

Author SHA1 Message Date
Pirmin Hinderling (fedora) 16428f2c16 Replace counter-arguments with possible answers and unify search inputs.
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>
2026-07-07 22:50:53 +02:00
19 changed files with 237 additions and 101 deletions
+6 -6
View File
@@ -1,6 +1,6 @@
# Faktenkompass Wissensdatenbank für überprüfbare Behauptungen # 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 ## Voraussetzungen
@@ -42,7 +42,7 @@ python manage.py migrate
python manage.py load_sample_data 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 ### 5. Admin-Zugang einrichten
@@ -255,7 +255,7 @@ ArgumentAtlas/
│ ├── settings.py │ ├── settings.py
│ └── urls.py │ └── urls.py
├── claims/ # Haupt-App ├── claims/ # Haupt-App
│ ├── models.py # Category, Claim, Source, EvidenceFile, CounterArgument, Tag │ ├── models.py # Category, Claim, Source, EvidenceFile, PossibleAnswer, Tag
│ ├── views.py │ ├── views.py
│ ├── urls.py │ ├── urls.py
│ ├── admin.py │ ├── admin.py
@@ -270,14 +270,14 @@ ArgumentAtlas/
- **Startseite:** Kategorien als Karten mit Behauptungsanzahl - **Startseite:** Kategorien als Karten mit Behauptungsanzahl
- **Kategorieseite:** Behauptungen filtern und durchsuchen - **Kategorieseite:** Behauptungen filtern und durchsuchen
- **Behauptungsdetail:** Status, Kurzantwort, Nachweise (Bilder/PDFs), Quellen, Gegenargumente, Tags - **Behauptungsdetail:** Status, Kurzantwort, Nachweise (Bilder/PDFs), Quellen, mögliche Antworten, Tags
- **Globale Suche:** Behauptungen über alle Kategorien hinweg - **Globale Suche:** Behauptungen über alle Kategorien hinweg
- **Admin:** Alle Inhalte über Django Admin pflegen - **Admin:** Alle Inhalte über Django Admin pflegen
- **SEO:** Sprechende URLs (`/kategorie/klimawandel/`, `/behauptung/...`) - **SEO:** Sprechende URLs (`/kategorie/klimawandel/`, `/behauptung/...`)
## Inhalte verwalten ## 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 ### Nachweise hochladen
@@ -305,7 +305,7 @@ export OPENAI_API_KEY=sk-...
1. Admin öffnen → **Fakten-Assistent** (Startseite oder unter Fakten-Entwürfe) 1. Admin öffnen → **Fakten-Assistent** (Startseite oder unter Fakten-Entwürfe)
2. Artikel-URL, Beschreibung und/oder Bild/PDF eingeben 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** 4. Entwurf prüfen, bearbeiten, ggf. **KI erneut fragen**
5. Erst mit **Veröffentlichen** wird der Eintrag live geschaltet 5. Erst mit **Veröffentlichen** wird der Eintrag live geschaltet
+17 -17
View File
@@ -4,12 +4,12 @@ from django.utils.html import format_html
from .admin_forms import ( from .admin_forms import (
ClaimAdminForm, ClaimAdminForm,
CounterArgumentInlineForm, PossibleAnswerInlineForm,
EvidenceFileInlineForm, EvidenceFileInlineForm,
SourceInlineForm, SourceInlineForm,
) )
from .admin_views import AssistantView, ReviewDraftView 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: class FaktenAdminMixin:
@@ -59,14 +59,14 @@ class SourceInline(FaktenAdminMixin, admin.StackedInline):
verbose_name_plural = 'Quellen' verbose_name_plural = 'Quellen'
class CounterArgumentInline(FaktenAdminMixin, admin.StackedInline): class PossibleAnswerInline(FaktenAdminMixin, admin.StackedInline):
model = CounterArgument model = PossibleAnswer
form = CounterArgumentInlineForm form = PossibleAnswerInlineForm
extra = 1 extra = 1
fields = ('argument', 'response') fields = ('answer',)
classes = ('collapse-open',) classes = ('collapse-open',)
verbose_name = 'Gegenargument' verbose_name = 'Mögliche Antwort'
verbose_name_plural = 'Gegenargumente' verbose_name_plural = 'Mögliche Antworten'
@admin.register(Tag) @admin.register(Tag)
@@ -115,7 +115,7 @@ class ClaimAdmin(FaktenAdminMixin, admin.ModelAdmin):
autocomplete_fields = ('category',) autocomplete_fields = ('category',)
save_on_top = True save_on_top = True
date_hierarchy = 'updated_at' date_hierarchy = 'updated_at'
inlines = [EvidenceFileInline, SourceInline, CounterArgumentInline] inlines = [EvidenceFileInline, SourceInline, PossibleAnswerInline]
readonly_fields = ('public_link', 'created_at', 'updated_at') readonly_fields = ('public_link', 'created_at', 'updated_at')
fieldsets = ( fieldsets = (
('Einordnung', { ('Einordnung', {
@@ -175,16 +175,16 @@ class SourceAdmin(FaktenAdminMixin, admin.ModelAdmin):
search_fields = ('title', 'organization', 'description') search_fields = ('title', 'organization', 'description')
@admin.register(CounterArgument) @admin.register(PossibleAnswer)
class CounterArgumentAdmin(FaktenAdminMixin, admin.ModelAdmin): class PossibleAnswerAdmin(FaktenAdminMixin, admin.ModelAdmin):
form = CounterArgumentInlineForm form = PossibleAnswerInlineForm
list_display = ('argument_preview', 'claim') list_display = ('answer_preview', 'claim')
list_filter = ('claim__category',) list_filter = ('claim__category',)
search_fields = ('argument', 'response') search_fields = ('answer', 'claim__title')
@admin.display(description='Gegenargument') @admin.display(description='Antwort')
def argument_preview(self, obj): def answer_preview(self, obj):
return obj.argument[:80] return obj.answer[:80]
@admin.register(ClaimDraft) @admin.register(ClaimDraft)
+4 -5
View File
@@ -1,6 +1,6 @@
from django import forms from django import forms
from .models import Claim, CounterArgument, EvidenceFile, Source from .models import Claim, PossibleAnswer, EvidenceFile, Source
class ClaimAdminForm(forms.ModelForm): class ClaimAdminForm(forms.ModelForm):
@@ -26,13 +26,12 @@ class SourceInlineForm(forms.ModelForm):
} }
class CounterArgumentInlineForm(forms.ModelForm): class PossibleAnswerInlineForm(forms.ModelForm):
class Meta: class Meta:
model = CounterArgument model = PossibleAnswer
fields = '__all__' fields = '__all__'
widgets = { widgets = {
'argument': forms.Textarea(attrs={'rows': 3, 'class': 'fk-admin-textarea'}), 'answer': forms.Textarea(attrs={'rows': 4, 'class': 'fk-admin-textarea'}),
'response': forms.Textarea(attrs={'rows': 4, 'class': 'fk-admin-textarea'}),
} }
+9 -9
View File
@@ -53,8 +53,8 @@ class ReviewDraftForm(forms.ModelForm):
required=False, required=False,
widget=forms.Textarea(attrs={'class': 'form-control font-monospace', 'rows': 6}), widget=forms.Textarea(attrs={'class': 'form-control font-monospace', 'rows': 6}),
) )
counter_arguments_json = forms.CharField( possible_answers_json = forms.CharField(
label='Gegenargumente (JSON)', label='Mögliche Antworten (JSON)',
required=False, required=False,
widget=forms.Textarea(attrs={'class': 'form-control font-monospace', 'rows': 6}), widget=forms.Textarea(attrs={'class': 'form-control font-monospace', 'rows': 6}),
) )
@@ -95,8 +95,8 @@ class ReviewDraftForm(forms.ModelForm):
self.fields['sources_json'].initial = json.dumps( self.fields['sources_json'].initial = json.dumps(
instance.sources or [], ensure_ascii=False, indent=2 instance.sources or [], ensure_ascii=False, indent=2
) )
self.fields['counter_arguments_json'].initial = json.dumps( self.fields['possible_answers_json'].initial = json.dumps(
instance.counter_arguments or [], ensure_ascii=False, indent=2 instance.possible_answers or [], ensure_ascii=False, indent=2
) )
if instance.category_slug: if instance.category_slug:
category = Category.objects.filter(slug=instance.category_slug).first() category = Category.objects.filter(slug=instance.category_slug).first()
@@ -119,23 +119,23 @@ class ReviewDraftForm(forms.ModelForm):
raise forms.ValidationError('Quellen müssen eine JSON-Liste sein.') raise forms.ValidationError('Quellen müssen eine JSON-Liste sein.')
return data return data
def clean_counter_arguments_json(self): def clean_possible_answers_json(self):
raw = (self.cleaned_data.get('counter_arguments_json') or '').strip() raw = (self.cleaned_data.get('possible_answers_json') or '').strip()
if not raw: if not raw:
return [] return []
try: try:
data = json.loads(raw) data = json.loads(raw)
except json.JSONDecodeError as exc: 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): 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 return data
def save(self, commit=True): def save(self, commit=True):
draft = super().save(commit=False) draft = super().save(commit=False)
draft.tags = self.cleaned_data['tags_text'] draft.tags = self.cleaned_data['tags_text']
draft.sources = self.cleaned_data['sources_json'] 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') category = self.cleaned_data.get('category_choice')
if category: if category:
draft.category_slug = category.slug draft.category_slug = category.slug
+9 -13
View File
@@ -1,6 +1,6 @@
from django.core.management.base import BaseCommand 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): class Command(BaseCommand):
@@ -96,10 +96,9 @@ class Command(BaseCommand):
url='https://www.rki.de/DE/Content/InfAZ/N/Neuartiges_Coronavirus/Impfen/Impfen_node.html', 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.', description='Offizielle Informationen des RKI zu Wirkweise und Sicherheit der Impfstoffe.',
) )
CounterArgument.objects.create( PossibleAnswer.objects.create(
claim=c1, claim=c1,
argument='Die mRNA ist künstlich und kann deshalb die Zelle dauerhaft verändern.', 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.',
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.',
) )
c2 = Claim.objects.create( c2 = Claim.objects.create(
@@ -142,10 +141,9 @@ class Command(BaseCommand):
url='https://www.ipcc.ch/report/ar6/syr/', url='https://www.ipcc.ch/report/ar6/syr/',
description='Aktueller Synthesebericht des Weltklimarats.', description='Aktueller Synthesebericht des Weltklimarats.',
) )
CounterArgument.objects.create( PossibleAnswer.objects.create(
claim=c1, claim=c1,
argument='Früher gab es auch warme und kalte Perioden ohne Industrie.', answer='Natürliche Schwankungen sind bekannt, aber die Geschwindigkeit der aktuellen Erwärmung passt zu Treibhausgasanstieg, nicht zu dominanten natürlichen Zyklen.',
response='Natürliche Schwankungen sind bekannt, aber die Geschwindigkeit der aktuellen Erwärmung passt zu Treibhausgasanstieg, nicht zu dominanten natürlichen Zyklen.',
) )
Claim.objects.create( Claim.objects.create(
@@ -173,10 +171,9 @@ class Command(BaseCommand):
detailed_explanation='Studien zeigen Szenarien mit nahezu 100 % erneuerbarem Strom, erfordern aber Infrastruktur und Sektorkopplung.', detailed_explanation='Studien zeigen Szenarien mit nahezu 100 % erneuerbarem Strom, erfordern aber Infrastruktur und Sektorkopplung.',
) )
c1.tags.add(tags['erneuerbare']) c1.tags.add(tags['erneuerbare'])
CounterArgument.objects.create( PossibleAnswer.objects.create(
claim=c1, claim=c1,
argument='Wenn die Sonne nicht scheint und der Wind nicht weht, fällt das Netz aus.', answer='Energiesysteme kombinieren Speicher, Netze, Importe und Reservekapazitäten statt auf eine einzige Quelle zu setzen.',
response='Energiesysteme kombinieren Speicher, Netze, Importe und Reservekapazitäten statt auf eine einzige Quelle zu setzen.',
) )
Claim.objects.create( Claim.objects.create(
@@ -255,8 +252,7 @@ class Command(BaseCommand):
detailed_explanation='Peer Review filtert Fehler, garantiert aber keine Wahrheit. Metaanalysen fassen viele Studien zusammen.', detailed_explanation='Peer Review filtert Fehler, garantiert aber keine Wahrheit. Metaanalysen fassen viele Studien zusammen.',
) )
c1.tags.add(tags['forschung']) c1.tags.add(tags['forschung'])
CounterArgument.objects.create( PossibleAnswer.objects.create(
claim=c1, claim=c1,
argument='Wenn eine Studie in einer renommierten Zeitschrift steht, ist sie bewiesen.', answer='Renommierte Journale publizieren auch widerlegte Ergebnisse. Die Qualität hängt von Replikation und Methodik ab.',
response='Renommierte Journale publizieren auch widerlegte Ergebnisse. Die Qualität hängt von Replikation und Methodik ab.',
) )
@@ -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 -8
View File
@@ -205,23 +205,22 @@ class Source(models.Model):
return self.title return self.title
class CounterArgument(models.Model): class PossibleAnswer(models.Model):
claim = models.ForeignKey( claim = models.ForeignKey(
Claim, Claim,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='counter_arguments', related_name='possible_answers',
verbose_name='Behauptung', verbose_name='Behauptung',
) )
argument = models.TextField('Gegenargument') answer = models.TextField('Antwort')
response = models.TextField('Antwort')
class Meta: class Meta:
verbose_name = 'Gegenargument' verbose_name = 'Mögliche Antwort'
verbose_name_plural = 'Gegenargumente' verbose_name_plural = 'Mögliche Antworten'
ordering = ['id'] ordering = ['id']
def __str__(self): def __str__(self):
return self.argument[:80] return self.answer[:80]
def draft_upload_path(instance, filename): def draft_upload_path(instance, filename):
@@ -286,7 +285,7 @@ class ClaimDraft(models.Model):
detailed_explanation = models.TextField('Ausführliche Erklärung', blank=True) detailed_explanation = models.TextField('Ausführliche Erklärung', blank=True)
tags = models.JSONField('Schlagwörter', default=list, blank=True) tags = models.JSONField('Schlagwörter', default=list, blank=True)
sources = models.JSONField('Quellen', 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) conversation = models.JSONField('Konversation', default=list, blank=True)
ai_raw_response = models.JSONField('KI-Rohantwort', blank=True, null=True) ai_raw_response = models.JSONField('KI-Rohantwort', blank=True, null=True)
error_message = models.TextField('Fehlermeldung', blank=True) error_message = models.TextField('Fehlermeldung', blank=True)
+18 -4
View File
@@ -20,6 +20,7 @@ Regeln:
- Quellen müssen echte, nachvollziehbare Referenzen sein (URL aus Eingabe bevorzugen). - 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. - 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. - Icons für neue Kategorien: Bootstrap Icons Klassen wie bi-thermometer-half, bi-virus, bi-lightning-charge.
- Unter possible_answers: 24 kurze, diskussionsfertige Antworten auf die Behauptung formulieren.
Antworte ausschließlich als gültiges JSON mit dieser Struktur: Antworte ausschließlich als gültiges JSON mit dieser Struktur:
{ {
@@ -46,10 +47,9 @@ Antworte ausschließlich als gültiges JSON mit dieser Struktur:
"description": "Kurzbeschreibung" "description": "Kurzbeschreibung"
} }
], ],
"counter_arguments": [ "possible_answers": [
{ {
"argument": "Häufiges Gegenargument", "answer": "Kurze, sachliche Antwort auf die Behauptung für die Diskussion"
"response": "Sachliche Antwort"
} }
], ],
"assistant_summary": "Kurze Zusammenfassung für den Admin (1-2 Sätze)" "assistant_summary": "Kurze Zusammenfassung für den Admin (1-2 Sätze)"
@@ -229,6 +229,18 @@ def generate_draft_payload(
return _validate_payload(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): def apply_payload_to_draft(draft, payload):
category = payload.get('category') or {} category = payload.get('category') or {}
claim = payload.get('claim') or {} claim = payload.get('claim') or {}
@@ -245,7 +257,9 @@ def apply_payload_to_draft(draft, payload):
draft.detailed_explanation = claim.get('detailed_explanation', '') draft.detailed_explanation = claim.get('detailed_explanation', '')
draft.tags = payload.get('tags') or [] draft.tags = payload.get('tags') or []
draft.sources = payload.get('sources') 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 draft.ai_raw_response = payload
summary = payload.get('assistant_summary', 'Entwurf wurde erstellt.') summary = payload.get('assistant_summary', 'Entwurf wurde erstellt.')
draft.add_message('assistant', summary) draft.add_message('assistant', summary)
+8 -6
View File
@@ -4,7 +4,7 @@ import uuid
from django.db import transaction from django.db import transaction
from django.utils.text import slugify 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): class DraftPublishError(Exception):
@@ -72,11 +72,13 @@ def publish_draft(draft):
description=source.get('description', ''), description=source.get('description', ''),
) )
for counter in draft.counter_arguments or []: for item in draft.possible_answers or []:
argument = (counter.get('argument') or '').strip() if isinstance(item, str):
response = (counter.get('response') or '').strip() answer = item.strip()
if argument and response: else:
CounterArgument.objects.create(claim=claim, argument=argument, response=response) answer = (item.get('answer') or item.get('response') or '').strip()
if answer:
PossibleAnswer.objects.create(claim=claim, answer=answer)
if draft.uploaded_file: if draft.uploaded_file:
_attach_uploaded_file(draft, claim) _attach_uploaded_file(draft, claim)
+1 -1
View File
@@ -59,7 +59,7 @@ def category_detail(request, slug):
def claim_detail(request, slug): def claim_detail(request, slug):
claim = get_object_or_404( claim = get_object_or_404(
Claim.objects.select_related('category').prefetch_related( Claim.objects.select_related('category').prefetch_related(
'tags', 'sources', 'counter_arguments', 'evidence_files' 'tags', 'sources', 'possible_answers', 'evidence_files'
), ),
slug=slug, slug=slug,
) )
+36
View File
@@ -252,6 +252,42 @@ a:hover .claim-card {
border-radius: 10px 0 0 10px; 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 { .btn-primary {
background-color: var(--fk-accent); background-color: var(--fk-accent);
border-color: var(--fk-accent); border-color: var(--fk-accent);
@@ -10,7 +10,7 @@
{% else %} {% else %}
<div class="fk-claim-help"> <div class="fk-claim-help">
Neue Behauptung anlegen: zuerst Titel und Kurzantwort ausfüllen, dann speichern. 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> </div>
{% endif %} {% endif %}
{{ block.super }} {{ block.super }}
+2 -2
View File
@@ -102,8 +102,8 @@
</div> </div>
<div class="field-box"> <div class="field-box">
<label for="{{ form.counter_arguments_json.id_for_label }}">{{ form.counter_arguments_json.label }}</label> <label for="{{ form.possible_answers_json.id_for_label }}">{{ form.possible_answers_json.label }}</label>
{{ form.counter_arguments_json }} {{ form.possible_answers_json }}
</div> </div>
<div class="actions-bar"> <div class="actions-bar">
+3 -3
View File
@@ -20,12 +20,12 @@
</div> </div>
</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"> <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>
<div class="col-12 col-sm-auto"> <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> <option value="">Alle Status</option>
{% for value, label in status_choices %} {% for value, label in status_choices %}
<option value="{{ value }}"{% if status == value %} selected{% endif %}>{{ label }}</option> <option value="{{ value }}"{% if status == value %} selected{% endif %}>{{ label }}</option>
+6 -17
View File
@@ -89,24 +89,13 @@
</section> </section>
{% endif %} {% endif %}
{% if claim.counter_arguments.all %} {% if claim.possible_answers.all %}
<section class="fk-section"> <section class="fk-section">
<p class="fk-section-label">Gegenargumente</p> <p class="fk-section-label">Mögliche Antworten</p>
<div class="accordion fk-accordion" id="counterArgs"> <div class="d-flex flex-column gap-2">
{% for ca in claim.counter_arguments.all %} {% for item in claim.possible_answers.all %}
<div class="accordion-item"> <div class="fk-card fk-section-body">
<h2 class="accordion-header"> {{ item.answer|linebreaks }}
<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>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
+1 -4
View File
@@ -6,10 +6,7 @@
<header class="mb-4"> <header class="mb-4">
<h1 class="fk-page-title">Themen</h1> <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"> <form method="get" action="{% url 'claims:home' %}" class="mb-2" data-live-search data-live-target="#home-results">
<div class="input-group"> {% include "claims/partials/search_input.html" with value=query aria_label="Kategorien durchsuchen" %}
<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>
</form> </form>
<div class="d-flex justify-content-between align-items-center"> <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> <a href="{% url 'claims:search' %}" class="small text-muted text-decoration-none">Alle Behauptungen</a>
@@ -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>
+4 -5
View File
@@ -5,13 +5,12 @@
{% block content %} {% block content %}
<h1 class="fk-page-title">Suche</h1> <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"> <div class="col-12">
<input type="search" id="q" name="q" class="form-control form-control-sm" {% include "claims/partials/search_input.html" with value=query placeholder="Behauptung suchen…" autofocus=True aria_label="Behauptung suchen" %}
placeholder="Behauptung suchen…" value="{{ query }}" autofocus autocomplete="off">
</div> </div>
<div class="col-6"> <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> <option value="">Alle Themen</option>
{% for cat in categories %} {% for cat in categories %}
<option value="{{ cat.slug }}"{% if category_slug == cat.slug %} selected{% endif %}>{{ cat.name }}</option> <option value="{{ cat.slug }}"{% if category_slug == cat.slug %} selected{% endif %}>{{ cat.name }}</option>
@@ -19,7 +18,7 @@
</select> </select>
</div> </div>
<div class="col-6"> <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> <option value="">Alle Status</option>
{% for value, label in status_choices %} {% for value, label in status_choices %}
<option value="{{ value }}"{% if status == value %} selected{% endif %}>{{ label }}</option> <option value="{{ value }}"{% if status == value %} selected{% endif %}>{{ label }}</option>