Add Geschichte (History) wiki-style feature with reorganized navigation

🆕 NEW FEATURES:
- Wiki-style Geschichte (History) section with rich text editor
- Image upload support for history pages
- Quill.js rich text editor with formatting options
- Slug-based URLs for SEO-friendly history pages
- Image galleries with descriptions and alt-text support

🔧 MODELS:
- GeschichteSeite: Main history pages with rich content
- GeschichteBild: Image attachments for history pages
- Auto-generated slugs, sorting, publishing controls

📝 TEMPLATES:
- geschichte/liste.html: Card-based overview of all history pages
- geschichte/detail.html: Full page view with image gallery
- geschichte/form.html: Rich text editor for creating/editing pages
- geschichte/bild_form.html: Image upload interface

🎨 UI IMPROVEMENTS:
- Reorganized navigation menu into logical groups:
  * Menschen & Finanzen (People & Finance)
  * Immobilien & Land (Real Estate & Land)
  * Verwaltung (Administration)
  * Geschichte (History)
- More compact menu design saving horizontal space
- Better grouping with dropdown headers

🛠️ TECHNICAL:
- Rich text editor with Quill.js integration
- Image upload with validation and optimization
- Permission-based access controls
- Responsive design for all screen sizes
- Proper breadcrumb navigation
- Auto-slug generation from titles
This commit is contained in:
2025-10-02 21:49:12 +02:00
parent 390bf697ee
commit 2961f376c3
10 changed files with 900 additions and 18 deletions

View File

@@ -1603,3 +1603,92 @@ class BackupTokenRegenerateForm(forms.Form):
label='Passwort',
help_text='Geben Sie Ihr Passwort ein, um neue Backup-Codes zu generieren'
)
class GeschichteSeiteForm(forms.ModelForm):
"""Form for creating and editing history pages"""
class Meta:
from .models import GeschichteSeite
model = GeschichteSeite
fields = ['titel', 'slug', 'inhalt', 'ist_veroeffentlicht', 'sortierung']
widgets = {
'titel': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'z.B. Gründung der Stiftung'
}),
'slug': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'z.B. gruendung-der-stiftung'
}),
'inhalt': forms.Textarea(attrs={
'class': 'form-control rich-text-editor',
'rows': 20,
'placeholder': 'Schreiben Sie hier den Inhalt der Geschichtsseite...'
}),
'ist_veroeffentlicht': forms.CheckboxInput(attrs={
'class': 'form-check-input'
}),
'sortierung': forms.NumberInput(attrs={
'class': 'form-control',
'min': 0
})
}
help_texts = {
'slug': 'URL-freundliche Version des Titels (nur Buchstaben, Zahlen und Bindestriche)',
'inhalt': 'Unterstützt Rich-Text-Formatierung, Bilder und Videos',
'sortierung': 'Niedrigere Zahlen erscheinen zuerst in der Navigation'
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Auto-generate slug from title if not provided
if not self.instance.pk:
self.fields['slug'].required = False
def clean_slug(self):
slug = self.cleaned_data.get('slug')
titel = self.cleaned_data.get('titel')
if not slug and titel:
# Auto-generate slug from title
from django.utils.text import slugify
slug = slugify(titel)
return slug
class GeschichteBildForm(forms.ModelForm):
"""Form for uploading images to history pages"""
class Meta:
from .models import GeschichteBild
model = GeschichteBild
fields = ['titel', 'bild', 'beschreibung', 'alt_text', 'sortierung']
widgets = {
'titel': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'z.B. Gründungsurkunde 1895'
}),
'bild': forms.ClearableFileInput(attrs={
'class': 'form-control'
}),
'beschreibung': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Beschreibung des Bildes...'
}),
'alt_text': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Alternativtext für Bildschirmleser'
}),
'sortierung': forms.NumberInput(attrs={
'class': 'form-control',
'min': 0
})
}
help_texts = {
'bild': 'Unterstützte Formate: JPG, PNG, GIF (max. 10MB)',
'alt_text': 'Wichtig für Barrierefreiheit',
'sortierung': 'Reihenfolge in der Bildergalerie'
}

View File

@@ -0,0 +1,56 @@
# Generated by Django 5.0.6 on 2025-10-02 19:48
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stiftung', '0036_force_semester_deadlines_update'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='GeschichteSeite',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('titel', models.CharField(max_length=200, verbose_name='Titel')),
('slug', models.SlugField(max_length=200, unique=True, verbose_name='URL-Slug')),
('inhalt', models.TextField(verbose_name='Inhalt')),
('erstellt_am', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
('aktualisiert_am', models.DateTimeField(auto_now=True, verbose_name='Aktualisiert am')),
('ist_veroeffentlicht', models.BooleanField(default=True, verbose_name='Veröffentlicht')),
('sortierung', models.IntegerField(default=0, verbose_name='Sortierung')),
('aktualisiert_von', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='geschichte_seiten_aktualisiert', to=settings.AUTH_USER_MODEL, verbose_name='Aktualisiert von')),
('erstellt_von', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='geschichte_seiten_erstellt', to=settings.AUTH_USER_MODEL, verbose_name='Erstellt von')),
],
options={
'verbose_name': 'Geschichte Seite',
'verbose_name_plural': 'Geschichte Seiten',
'ordering': ['sortierung', 'titel'],
},
),
migrations.CreateModel(
name='GeschichteBild',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('titel', models.CharField(max_length=200, verbose_name='Bildtitel')),
('bild', models.ImageField(upload_to='geschichte/bilder/%Y/%m/', verbose_name='Bild')),
('beschreibung', models.TextField(blank=True, verbose_name='Beschreibung')),
('alt_text', models.CharField(blank=True, max_length=200, verbose_name='Alt-Text')),
('hochgeladen_am', models.DateTimeField(auto_now_add=True, verbose_name='Hochgeladen am')),
('sortierung', models.IntegerField(default=0, verbose_name='Sortierung')),
('hochgeladen_von', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Hochgeladen von')),
('seite', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bilder', to='stiftung.geschichteseite', verbose_name='Geschichte Seite')),
],
options={
'verbose_name': 'Geschichte Bild',
'verbose_name_plural': 'Geschichte Bilder',
'ordering': ['sortierung', 'titel'],
},
),
]

View File

@@ -2856,3 +2856,87 @@ class VierteljahresNachweis(models.Model):
except VierteljahresNachweis.DoesNotExist:
pass
return None
class GeschichteSeite(models.Model):
"""Wiki-style pages for foundation history"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
titel = models.CharField(max_length=200, verbose_name="Titel")
slug = models.SlugField(max_length=200, unique=True, verbose_name="URL-Slug")
inhalt = models.TextField(verbose_name="Inhalt")
# Metadata
erstellt_am = models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am")
aktualisiert_am = models.DateTimeField(auto_now=True, verbose_name="Aktualisiert am")
erstellt_von = models.ForeignKey(
'auth.User',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='geschichte_seiten_erstellt',
verbose_name="Erstellt von"
)
aktualisiert_von = models.ForeignKey(
'auth.User',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='geschichte_seiten_aktualisiert',
verbose_name="Aktualisiert von"
)
# Options
ist_veroeffentlicht = models.BooleanField(default=True, verbose_name="Veröffentlicht")
sortierung = models.IntegerField(default=0, verbose_name="Sortierung")
class Meta:
verbose_name = "Geschichte Seite"
verbose_name_plural = "Geschichte Seiten"
ordering = ['sortierung', 'titel']
def __str__(self):
return self.titel
def get_absolute_url(self):
from django.urls import reverse
return reverse('stiftung:geschichte_detail', kwargs={'slug': self.slug})
class GeschichteBild(models.Model):
"""Images for history pages"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
seite = models.ForeignKey(
GeschichteSeite,
on_delete=models.CASCADE,
related_name='bilder',
verbose_name="Geschichte Seite"
)
titel = models.CharField(max_length=200, verbose_name="Bildtitel")
bild = models.ImageField(
upload_to='geschichte/bilder/%Y/%m/',
verbose_name="Bild"
)
beschreibung = models.TextField(blank=True, verbose_name="Beschreibung")
alt_text = models.CharField(max_length=200, blank=True, verbose_name="Alt-Text")
# Metadata
hochgeladen_am = models.DateTimeField(auto_now_add=True, verbose_name="Hochgeladen am")
hochgeladen_von = models.ForeignKey(
'auth.User',
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name="Hochgeladen von"
)
sortierung = models.IntegerField(default=0, verbose_name="Sortierung")
class Meta:
verbose_name = "Geschichte Bild"
verbose_name_plural = "Geschichte Bilder"
ordering = ['sortierung', 'titel']
def __str__(self):
return f"{self.titel} ({self.seite.titel})"

View File

@@ -385,4 +385,10 @@ urlpatterns = [
views.quarterly_confirmation_reset,
name="quarterly_confirmation_reset",
),
# Geschichte URLs
path("geschichte/", views.geschichte_list, name="geschichte_list"),
path("geschichte/neu/", views.geschichte_create, name="geschichte_create"),
path("geschichte/<slug:slug>/", views.geschichte_detail, name="geschichte_detail"),
path("geschichte/<slug:slug>/bearbeiten/", views.geschichte_edit, name="geschichte_edit"),
path("geschichte/<slug:slug>/bild-upload/", views.geschichte_bild_upload, name="geschichte_bild_upload"),
]

View File

@@ -8030,3 +8030,125 @@ def backup_tokens(request):
}
return render(request, 'stiftung/auth/backup_tokens_manage.html', context)
# Geschichte (History) Views
from .models import GeschichteSeite, GeschichteBild
from .forms import GeschichteSeiteForm, GeschichteBildForm
@login_required
def geschichte_list(request):
"""List all published history pages"""
seiten = GeschichteSeite.objects.filter(ist_veroeffentlicht=True).order_by('sortierung', 'titel')
context = {
'seiten': seiten,
'title': 'Geschichte der Stiftung'
}
return render(request, 'stiftung/geschichte/liste.html', context)
@login_required
def geschichte_detail(request, slug):
"""Display a specific history page"""
seite = get_object_or_404(GeschichteSeite, slug=slug, ist_veroeffentlicht=True)
bilder = seite.bilder.all().order_by('sortierung', 'titel')
context = {
'seite': seite,
'bilder': bilder,
'title': seite.titel
}
return render(request, 'stiftung/geschichte/detail.html', context)
@login_required
def geschichte_create(request):
"""Create a new history page"""
if not request.user.has_perm('stiftung.add_geschichteseite'):
messages.error(request, 'Sie haben keine Berechtigung, neue Geschichtsseiten zu erstellen.')
return redirect('stiftung:geschichte_list')
if request.method == 'POST':
form = GeschichteSeiteForm(request.POST)
if form.is_valid():
seite = form.save(commit=False)
seite.erstellt_von = request.user
seite.aktualisiert_von = request.user
seite.save()
messages.success(request, f'Geschichtsseite "{seite.titel}" wurde erfolgreich erstellt.')
return redirect('stiftung:geschichte_detail', slug=seite.slug)
else:
form = GeschichteSeiteForm()
context = {
'form': form,
'title': 'Neue Geschichtsseite'
}
return render(request, 'stiftung/geschichte/form.html', context)
@login_required
def geschichte_edit(request, slug):
"""Edit an existing history page"""
seite = get_object_or_404(GeschichteSeite, slug=slug)
if not request.user.has_perm('stiftung.change_geschichteseite'):
messages.error(request, 'Sie haben keine Berechtigung, diese Geschichtsseite zu bearbeiten.')
return redirect('stiftung:geschichte_detail', slug=slug)
if request.method == 'POST':
form = GeschichteSeiteForm(request.POST, instance=seite)
if form.is_valid():
seite = form.save(commit=False)
seite.aktualisiert_von = request.user
seite.save()
messages.success(request, f'Geschichtsseite "{seite.titel}" wurde erfolgreich aktualisiert.')
return redirect('stiftung:geschichte_detail', slug=seite.slug)
else:
form = GeschichteSeiteForm(instance=seite)
context = {
'form': form,
'seite': seite,
'title': f'Bearbeiten: {seite.titel}'
}
return render(request, 'stiftung/geschichte/form.html', context)
@login_required
def geschichte_bild_upload(request, slug):
"""Upload images to a history page"""
seite = get_object_or_404(GeschichteSeite, slug=slug)
if not request.user.has_perm('stiftung.add_geschichtebild'):
messages.error(request, 'Sie haben keine Berechtigung, Bilder hochzuladen.')
return redirect('stiftung:geschichte_detail', slug=slug)
if request.method == 'POST':
form = GeschichteBildForm(request.POST, request.FILES)
if form.is_valid():
bild = form.save(commit=False)
bild.seite = seite
bild.hochgeladen_von = request.user
bild.save()
messages.success(request, f'Bild "{bild.titel}" wurde erfolgreich hochgeladen.')
return redirect('stiftung:geschichte_detail', slug=slug)
else:
form = GeschichteBildForm()
context = {
'form': form,
'seite': seite,
'title': f'Bild hochladen: {seite.titel}'
}
return render(request, 'stiftung/geschichte/bild_form.html', context)