initial commit

This commit is contained in:
Pirmin Hinderling (fedora)
2026-07-07 13:02:49 +02:00
commit 2c7fb7d030
30 changed files with 1666 additions and 0 deletions
View File
+84
View File
@@ -0,0 +1,84 @@
from django.contrib import admin
from .models import Category, Claim, CounterArgument, EvidenceFile, Source, Tag
class EvidenceFileInline(admin.TabularInline):
model = EvidenceFile
extra = 1
fields = ('title', 'file', 'caption', 'order')
class SourceInline(admin.TabularInline):
model = Source
extra = 1
class CounterArgumentInline(admin.StackedInline):
model = CounterArgument
extra = 1
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
list_display = ('name', 'slug')
prepopulated_fields = {'slug': ('name',)}
search_fields = ('name',)
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'slug', 'claim_count', 'created_at')
prepopulated_fields = {'slug': ('name',)}
search_fields = ('name', 'description')
@admin.display(description='Anzahl Behauptungen')
def claim_count(self, obj):
return obj.claims.count()
@admin.register(Claim)
class ClaimAdmin(admin.ModelAdmin):
list_display = ('title', 'category', 'status', 'evidence_level', 'updated_at')
list_filter = ('category', 'status', 'evidence_level', 'tags')
search_fields = ('title', 'short_answer', 'detailed_explanation')
prepopulated_fields = {'slug': ('title',)}
filter_horizontal = ('tags',)
inlines = [EvidenceFileInline, SourceInline, CounterArgumentInline]
fieldsets = (
(None, {
'fields': ('category', 'title', 'slug', 'status', 'evidence_level', 'tags'),
}),
('Inhalt', {
'fields': ('short_answer', 'detailed_explanation'),
}),
)
@admin.register(EvidenceFile)
class EvidenceFileAdmin(admin.ModelAdmin):
list_display = ('display_title', 'claim', 'file_extension_display', 'order', 'created_at')
list_filter = ('claim__category',)
search_fields = ('title', 'caption', 'claim__title')
@admin.display(description='Typ')
def file_extension_display(self, obj):
return obj.file_extension.upper()
@admin.register(Source)
class SourceAdmin(admin.ModelAdmin):
list_display = ('title', 'organization', 'claim')
list_filter = ('claim__category',)
search_fields = ('title', 'organization', 'description')
@admin.register(CounterArgument)
class CounterArgumentAdmin(admin.ModelAdmin):
list_display = ('argument_preview', 'claim')
list_filter = ('claim__category',)
search_fields = ('argument', 'response')
@admin.display(description='Gegenargument')
def argument_preview(self, obj):
return obj.argument[:80]
+7
View File
@@ -0,0 +1,7 @@
from django.apps import AppConfig
class ClaimsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'claims'
verbose_name = 'Faktenkompass Inhalte'
View File
@@ -0,0 +1,277 @@
from django.core.management.base import BaseCommand
from claims.models import Category, Claim, CounterArgument, Source, Tag
class Command(BaseCommand):
help = 'Lädt Beispieldaten für Faktenkompass (Kategorien, Behauptungen, Quellen).'
def handle(self, *args, **options):
if Category.objects.exists():
self.stdout.write(self.style.WARNING('Datenbank enthält bereits Daten. Abbruch.'))
return
self.stdout.write('Erstelle Beispieldaten…')
tags = {
'impfstoff': Tag.objects.create(name='Impfstoff'),
'co2': Tag.objects.create(name='CO₂'),
'erneuerbare': Tag.objects.create(name='Erneuerbare Energien'),
'migration': Tag.objects.create(name='Migration'),
'inflation': Tag.objects.create(name='Inflation'),
'forschung': Tag.objects.create(name='Forschung'),
'gesundheit': Tag.objects.create(name='Gesundheit'),
'klima': Tag.objects.create(name='Klima'),
}
categories = {
'corona': Category.objects.create(
name='Corona',
description='Behauptungen zu COVID-19, Impfungen, Maßnahmen und Langzeitfolgen mit Quellen aus Medizin und Epidemiologie.',
icon='bi-virus',
),
'klimawandel': Category.objects.create(
name='Klimawandel',
description='Fakten zur globalen Erwärmung, Treibhausgasen und Klimafolgen auf Basis wissenschaftlicher Erkenntnisse.',
icon='bi-thermometer-half',
),
'energie': Category.objects.create(
name='Energie',
description='Aussagen zu Stromerzeugung, erneuerbaren Energien, Atomkraft und Energiesicherheit.',
icon='bi-lightning-charge',
),
'migration': Category.objects.create(
name='Migration',
description='Überprüfbare Informationen zu Zuwanderung, Asyl, Integration und demografischen Entwicklungen.',
icon='bi-people',
),
'wirtschaft': Category.objects.create(
name='Wirtschaft',
description='Behauptungen zu Inflation, Staatsverschuldung, Arbeitsmarkt und wirtschaftlicher Entwicklung.',
icon='bi-graph-up',
),
'politik': Category.objects.create(
name='Politik',
description='Häufige politische Aussagen und deren empirische Überprüfung sachlich und quellenbasiert.',
icon='bi-bank',
),
'wissenschaft': Category.objects.create(
name='Wissenschaft',
description='Grundlagen wissenschaftlicher Methodik, Peer Review und typische Missverständnisse über Forschung.',
icon='bi-mortarboard',
),
}
self._create_corona_claims(categories['corona'], tags)
self._create_climate_claims(categories['klimawandel'], tags)
self._create_energy_claims(categories['energie'], tags)
self._create_migration_claims(categories['migration'], tags)
self._create_economy_claims(categories['wirtschaft'], tags)
self._create_politics_claims(categories['politik'], tags)
self._create_science_claims(categories['wissenschaft'], tags)
self.stdout.write(self.style.SUCCESS(
f'Fertig: {Category.objects.count()} Kategorien, '
f'{Claim.objects.count()} Behauptungen, '
f'{Source.objects.count()} Quellen.'
))
def _create_corona_claims(self, category, tags):
c1 = Claim.objects.create(
category=category,
title='mRNA-Impfstoffe verändern die menschliche DNA dauerhaft',
status=Claim.Status.FALSCH,
short_answer='mRNA-Impfstoffe gelangen nicht in den Zellkern und können die DNA nicht verändern. Die mRNA wird nach wenigen Tagen abgebaut.',
detailed_explanation=(
'Die mRNA in COVID-19-Impfstoffen dient als Bauanleitung für das Spike-Protein. '
'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(
claim=c1,
title='Zusammenfassung zur Sicherheit von COVID-19-Impfstoffen',
organization='Robert Koch-Institut (RKI)',
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(
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.',
)
c2 = Claim.objects.create(
category=category,
title='COVID-19 ist nicht gefährlicher als eine normale Grippe',
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(
claim=c2,
title='COVID-19: Fallzahlen und Sterblichkeit in Deutschland',
organization='Statistisches Bundesamt (Destatis)',
url='https://www.destatis.de/DE/Themen/Gesellschaft-Umwelt/Gesundheit/Todesursachen/_inhalt.html',
description='Amtliche Sterblichkeitsstatistiken und Sonderanalysen zur Pandemie.',
)
Claim.objects.create(
category=category,
title='Masken im Alltag schützen nachweislich vor Ansteckung mit respirativen Viren',
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):
c1 = Claim.objects.create(
category=category,
title='Der Klimawandel wird hauptsächlich durch natürliche Zyklen verursacht',
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(
claim=c1,
title='AR6 Synthesis Report: Climate Change 2023',
organization='Zwischenstaatlicher Ausschuss für Klimaänderungen (IPCC)',
url='https://www.ipcc.ch/report/ar6/syr/',
description='Aktueller Synthesebericht des Weltklimarats.',
)
CounterArgument.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.',
)
Claim.objects.create(
category=category,
title='Es gibt keinen wissenschaftlichen Konsens zum menschengemachten Klimawandel',
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(
category=category,
title='Extremwetterereignisse nehmen in Häufigkeit und Intensität zu',
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):
c1 = Claim.objects.create(
category=category,
title='Deutschland kann seinen Strombedarf ausschließlich mit Solar und Wind decken',
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(
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.',
)
Claim.objects.create(
category=category,
title='Atomkraft ist in Deutschland klimaneutral und deshalb unverzichtbar',
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(
category=category,
title='Der Ausbau erneuerbarer Energien treibt die Strompreise allein massiv nach oben',
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):
c1 = Claim.objects.create(
category=category,
title='Die Mehrheit der Migranten in Deutschland ist arbeitslos und lebt von Sozialleistungen',
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(
claim=c1,
title='Zuwanderung und Arbeitsmarkt',
organization='Institut für Arbeitsmarkt- und Berufsforschung (IAB)',
url='https://www.iab.de/',
description='Forschung des IAB zu Migration und Beschäftigung.',
)
Claim.objects.create(
category=category,
title='Deutschland hat die höchste Zuwanderung in Europa',
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):
Claim.objects.create(
category=category,
title='Die Inflation in Deutschland wird allein durch Lohnerhöhungen verursacht',
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(
category=category,
title='Deutschland ist der größte Exporteur der Welt',
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):
Claim.objects.create(
category=category,
title='Wahlbeteiligung in Deutschland sinkt seit Jahrzehnten kontinuierlich',
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):
c1 = Claim.objects.create(
category=category,
title='Eine einzelne Studie beweist wissenschaftlichen Konsens',
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(
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.',
)
+95
View File
@@ -0,0 +1,95 @@
# Generated by Django 5.2.15 on 2026-07-07 10:32
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Category',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('slug', models.SlugField(blank=True, max_length=220, unique=True, verbose_name='Slug')),
('description', models.TextField(verbose_name='Beschreibung')),
('icon', models.CharField(blank=True, help_text='z. B. bi-thermometer-half', max_length=50, verbose_name='Icon (Bootstrap Icons Klasse)')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
],
options={
'verbose_name': 'Kategorie',
'verbose_name_plural': 'Kategorien',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Tag',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True, verbose_name='Name')),
('slug', models.SlugField(blank=True, max_length=120, unique=True, verbose_name='Slug')),
],
options={
'verbose_name': 'Schlagwort',
'verbose_name_plural': 'Schlagwörter',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Claim',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=300, verbose_name='Titel (Behauptung)')),
('slug', models.SlugField(blank=True, max_length=320, unique=True, verbose_name='Slug')),
('status', models.CharField(choices=[('falsch', 'Falsch'), ('irrefuehrend', 'Irreführend'), ('teilweise_richtig', 'Teilweise richtig'), ('unbelegt', 'Unbelegt'), ('wissenschaftlicher_konsens', 'Wissenschaftlicher Konsens'), ('offene_frage', 'Offene Frage')], default='unbelegt', max_length=40, verbose_name='Status')),
('short_answer', models.TextField(verbose_name='Kurzantwort')),
('detailed_explanation', models.TextField(blank=True, verbose_name='Ausführliche Erklärung')),
('evidence_level', models.CharField(choices=[('sehr_stark', 'Sehr stark'), ('stark', 'Stark'), ('mittel', 'Mittel'), ('schwach', 'Schwach')], default='mittel', max_length=20, verbose_name='Evidenzlevel')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Aktualisiert am')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='claims', to='claims.category', verbose_name='Kategorie')),
('tags', models.ManyToManyField(blank=True, to='claims.tag', verbose_name='Schlagwörter')),
],
options={
'verbose_name': 'Behauptung',
'verbose_name_plural': 'Behauptungen',
'ordering': ['-updated_at'],
},
),
migrations.CreateModel(
name='CounterArgument',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('argument', models.TextField(verbose_name='Gegenargument')),
('response', models.TextField(verbose_name='Antwort')),
('claim', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='counter_arguments', to='claims.claim', verbose_name='Behauptung')),
],
options={
'verbose_name': 'Gegenargument',
'verbose_name_plural': 'Gegenargumente',
'ordering': ['id'],
},
),
migrations.CreateModel(
name='Source',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=300, verbose_name='Titel')),
('organization', models.CharField(max_length=200, verbose_name='Autor/Organisation')),
('url', models.URLField(max_length=500, verbose_name='Link')),
('description', models.TextField(blank=True, verbose_name='Beschreibung')),
('claim', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sources', to='claims.claim', verbose_name='Behauptung')),
],
options={
'verbose_name': 'Quelle',
'verbose_name_plural': 'Quellen',
'ordering': ['id'],
},
),
]
+33
View File
@@ -0,0 +1,33 @@
# Generated by Django 5.2.15 on 2026-07-07 10:59
import claims.models
import django.core.validators
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('claims', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='EvidenceFile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(blank=True, max_length=200, verbose_name='Titel')),
('file', models.FileField(help_text='Bilder (JPG, PNG, GIF, WebP, SVG) oder PDF', upload_to=claims.models.evidence_upload_path, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'pdf'])], verbose_name='Datei')),
('caption', models.CharField(blank=True, max_length=500, verbose_name='Bildunterschrift')),
('order', models.PositiveSmallIntegerField(default=0, verbose_name='Reihenfolge')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Hochgeladen am')),
('claim', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='evidence_files', to='claims.claim', verbose_name='Behauptung')),
],
options={
'verbose_name': 'Nachweis',
'verbose_name_plural': 'Nachweise',
'ordering': ['order', 'id'],
},
),
]
View File
+231
View File
@@ -0,0 +1,231 @@
from django.core.validators import FileExtensionValidator
from django.db import models
from django.urls import reverse
from django.utils.text import slugify
EVIDENCE_FILE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'pdf']
IMAGE_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'}
class Tag(models.Model):
name = models.CharField('Name', max_length=100, unique=True)
slug = models.SlugField('Slug', max_length=120, unique=True, blank=True)
class Meta:
verbose_name = 'Schlagwort'
verbose_name_plural = 'Schlagwörter'
ordering = ['name']
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
class Category(models.Model):
name = models.CharField('Name', max_length=200)
slug = models.SlugField('Slug', max_length=220, unique=True, blank=True)
description = models.TextField('Beschreibung')
icon = models.CharField(
'Icon (Bootstrap Icons Klasse)',
max_length=50,
blank=True,
help_text='z. B. bi-thermometer-half',
)
created_at = models.DateTimeField('Erstellt am', auto_now_add=True)
class Meta:
verbose_name = 'Kategorie'
verbose_name_plural = 'Kategorien'
ordering = ['name']
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse('claims:category_detail', kwargs={'slug': self.slug})
@property
def claim_count(self):
return self.claims.count()
class Claim(models.Model):
class Status(models.TextChoices):
FALSCH = 'falsch', 'Falsch'
IRREFUEHREND = 'irrefuehrend', 'Irreführend'
TEILWEISE_RICHTIG = 'teilweise_richtig', 'Teilweise richtig'
UNBELEGT = 'unbelegt', 'Unbelegt'
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,
related_name='claims',
verbose_name='Kategorie',
)
title = models.CharField('Titel (Behauptung)', max_length=300)
slug = models.SlugField('Slug', max_length=320, unique=True, blank=True)
status = models.CharField(
'Status',
max_length=40,
choices=Status.choices,
default=Status.UNBELEGT,
)
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)
class Meta:
verbose_name = 'Behauptung'
verbose_name_plural = 'Behauptungen'
ordering = ['-updated_at']
def __str__(self):
return self.title
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(self.title)
slug = base_slug
counter = 1
while Claim.objects.filter(slug=slug).exclude(pk=self.pk).exists():
slug = f'{base_slug}-{counter}'
counter += 1
self.slug = slug
super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse('claims:claim_detail', kwargs={'slug': self.slug})
def get_status_badge_class(self):
mapping = {
self.Status.FALSCH: 'danger',
self.Status.IRREFUEHREND: 'warning',
self.Status.TEILWEISE_RICHTIG: 'info',
self.Status.UNBELEGT: 'secondary',
self.Status.WISSENSCHAFTLICHER_KONSENS: 'success',
self.Status.OFFENE_FRAGE: 'primary',
}
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):
return f'evidence/{instance.claim.slug}/{filename}'
class EvidenceFile(models.Model):
claim = models.ForeignKey(
Claim,
on_delete=models.CASCADE,
related_name='evidence_files',
verbose_name='Behauptung',
)
title = models.CharField('Titel', max_length=200, blank=True)
file = models.FileField(
'Datei',
upload_to=evidence_upload_path,
validators=[FileExtensionValidator(allowed_extensions=EVIDENCE_FILE_EXTENSIONS)],
help_text='Bilder (JPG, PNG, GIF, WebP, SVG) oder PDF',
)
caption = models.CharField('Bildunterschrift', max_length=500, blank=True)
order = models.PositiveSmallIntegerField('Reihenfolge', default=0)
created_at = models.DateTimeField('Hochgeladen am', auto_now_add=True)
class Meta:
verbose_name = 'Nachweis'
verbose_name_plural = 'Nachweise'
ordering = ['order', 'id']
def __str__(self):
return self.display_title
@property
def display_title(self):
if self.title:
return self.title
return self.file.name.rsplit('/', 1)[-1]
@property
def file_extension(self):
return self.file.name.rsplit('.', 1)[-1].lower()
@property
def is_image(self):
return self.file_extension in IMAGE_EXTENSIONS
@property
def is_pdf(self):
return self.file_extension == 'pdf'
class Source(models.Model):
claim = models.ForeignKey(
Claim,
on_delete=models.CASCADE,
related_name='sources',
verbose_name='Behauptung',
)
title = models.CharField('Titel', max_length=300)
organization = models.CharField('Autor/Organisation', max_length=200)
url = models.URLField('Link', max_length=500)
description = models.TextField('Beschreibung', blank=True)
class Meta:
verbose_name = 'Quelle'
verbose_name_plural = 'Quellen'
ordering = ['id']
def __str__(self):
return self.title
class CounterArgument(models.Model):
claim = models.ForeignKey(
Claim,
on_delete=models.CASCADE,
related_name='counter_arguments',
verbose_name='Behauptung',
)
argument = models.TextField('Gegenargument')
response = models.TextField('Antwort')
class Meta:
verbose_name = 'Gegenargument'
verbose_name_plural = 'Gegenargumente'
ordering = ['id']
def __str__(self):
return self.argument[:80]
View File
+15
View File
@@ -0,0 +1,15 @@
from django import template
register = template.Library()
@register.filter
def status_label(value):
from claims.models import Claim
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)
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+12
View File
@@ -0,0 +1,12 @@
from django.urls import path
from . import views
app_name = 'claims'
urlpatterns = [
path('', views.home, name='home'),
path('suche/', views.search, name='search'),
path('kategorie/<slug:slug>/', views.category_detail, name='category_detail'),
path('behauptung/<slug:slug>/', views.claim_detail, name='claim_detail'),
]
+102
View File
@@ -0,0 +1,102 @@
from django.db.models import Q
from django.shortcuts import get_object_or_404, render
from .models import Category, Claim
def home(request):
query = request.GET.get('q', '').strip()
categories = Category.objects.all()
if query:
categories = categories.filter(
Q(name__icontains=query) | Q(description__icontains=query)
)
context = {
'categories': categories,
'query': query,
'total_claims': Claim.objects.count(),
}
return render(request, 'claims/home.html', context)
def category_detail(request, slug):
category = get_object_or_404(Category, slug=slug)
query = request.GET.get('q', '').strip()
status = request.GET.get('status', '').strip()
claims = category.claims.select_related('category').prefetch_related('tags')
if query:
claims = claims.filter(
Q(title__icontains=query)
| Q(short_answer__icontains=query)
| Q(tags__name__icontains=query)
).distinct()
if status and status in dict(Claim.Status.choices):
claims = claims.filter(status=status)
context = {
'category': category,
'claims': claims,
'query': query,
'status': status,
'status_choices': Claim.Status.choices,
}
return render(request, 'claims/category_detail.html', context)
def claim_detail(request, slug):
claim = get_object_or_404(
Claim.objects.select_related('category').prefetch_related(
'tags', 'sources', 'counter_arguments', 'evidence_files'
),
slug=slug,
)
related_claims = (
Claim.objects.filter(category=claim.category)
.exclude(pk=claim.pk)
.select_related('category')[:3]
)
context = {
'claim': claim,
'related_claims': related_claims,
}
return render(request, 'claims/claim_detail.html', context)
def search(request):
query = request.GET.get('q', '').strip()
category_slug = request.GET.get('kategorie', '').strip()
status = request.GET.get('status', '').strip()
claims = Claim.objects.select_related('category').prefetch_related('tags')
if query:
claims = claims.filter(
Q(title__icontains=query)
| Q(short_answer__icontains=query)
| Q(detailed_explanation__icontains=query)
| Q(tags__name__icontains=query)
).distinct()
if category_slug:
claims = claims.filter(category__slug=category_slug)
if status and status in dict(Claim.Status.choices):
claims = claims.filter(status=status)
categories = Category.objects.all()
context = {
'claims': claims if query or category_slug or status else None,
'query': query,
'category_slug': category_slug,
'status': status,
'categories': categories,
'status_choices': Claim.Status.choices,
'result_count': claims.count() if query or category_slug or status else 0,
}
return render(request, 'claims/search.html', context)