diff --git a/app/stiftung/forms.py b/app/stiftung/forms.py index 7e7c2ad..10517b5 100644 --- a/app/stiftung/forms.py +++ b/app/stiftung/forms.py @@ -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' + } diff --git a/app/stiftung/migrations/0037_add_geschichte_models.py b/app/stiftung/migrations/0037_add_geschichte_models.py new file mode 100644 index 0000000..9152393 --- /dev/null +++ b/app/stiftung/migrations/0037_add_geschichte_models.py @@ -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'], + }, + ), + ] diff --git a/app/stiftung/models.py b/app/stiftung/models.py index 341e85b..33b8990 100644 --- a/app/stiftung/models.py +++ b/app/stiftung/models.py @@ -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})" diff --git a/app/stiftung/urls.py b/app/stiftung/urls.py index 8b58c57..f0405c3 100644 --- a/app/stiftung/urls.py +++ b/app/stiftung/urls.py @@ -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//", views.geschichte_detail, name="geschichte_detail"), + path("geschichte//bearbeiten/", views.geschichte_edit, name="geschichte_edit"), + path("geschichte//bild-upload/", views.geschichte_bild_upload, name="geschichte_bild_upload"), ] diff --git a/app/stiftung/views.py b/app/stiftung/views.py index 856fa0d..ad1efd8 100644 --- a/app/stiftung/views.py +++ b/app/stiftung/views.py @@ -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) diff --git a/app/templates/base.html b/app/templates/base.html index a8eca99..a584f20 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -528,11 +528,14 @@ Dashboard + +