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:
@@ -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'
|
||||
}
|
||||
|
||||
56
app/stiftung/migrations/0037_add_geschichte_models.py
Normal file
56
app/stiftung/migrations/0037_add_geschichte_models.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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})"
|
||||
|
||||
@@ -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"),
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user