implement report feature

This commit is contained in:
Maximilian 2025-06-10 12:46:53 +02:00
parent 4218ee6b7d
commit d6d47f714a
31 changed files with 2472 additions and 8 deletions

View file

@ -65,14 +65,14 @@ JAZZMIN_SETTINGS = {
# List of apps (and/or models) to base side menu ordering off of (does not need to contain all apps/models)
# "order_with_respect_to": ["auth", "books", "books.author", "books.book"],
# Custom links to append to app groups, keyed on app name
# "custom_links": {
# "books": [{
# "name": "Make Messages",
# "url": "make_messages",
# "icon": "fas fa-comments",
# "permissions": ["books.view_book"]
# }]
# },
"custom_links": {
"reports": [{
"name": "Reports Dashboard",
"url": "/admin/reports/",
"icon": "fas fa-chart-bar",
"permissions": ["is_staff"]
}]
},
# Custom icons for side menu apps/models See https://fontawesome.com/icons?d=gallery&m=free&v=5.0.0,5.0.1,5.0.10,5.0.11,5.0.12,5.0.13,5.0.2,5.0.3,5.0.4,5.0.5,5.0.6,5.0.7,5.0.8,5.0.9,5.1.0,5.1.1,5.2.0,5.3.0,5.3.1,5.4.0,5.4.1,5.4.2,5.13.0,5.12.0,5.11.2,5.11.1,5.10.0,5.9.0,5.8.2,5.8.1,5.7.2,5.7.1,5.7.0,5.6.3,5.5.0,5.4.2
# for the full list of 5.13.0 free icon classes
"icons": {
@ -89,6 +89,9 @@ JAZZMIN_SETTINGS = {
"contact.Contact": "fas fa-solid fa-address-card",
"contact.ContactTag": "fas fa-solid fa-tags",
"sendemail.Emailadress": "fas fa-solid fa-envelope",
"reports.AutomaticReport": "fas fa-solid fa-chart-line",
"reports.ReportLog": "fas fa-solid fa-history",
"reports": "fas fa-solid fa-chart-bar",
},
# Icons that are used when one is not manually specified
# "default_icon_parents": "fas fa-chevron-circle-right",

View file

@ -85,6 +85,7 @@ INSTALLED_APPS = [
"contact",
"costs",
"export",
"reports",
"sendemail",
]

View file

@ -14,6 +14,7 @@ urlpatterns = [
path("export/", include("export.urls")),
# Admin
path("admin/", admin.site.urls),
path("admin/reports/", include("reports.urls", namespace="reports")),
# Allauth
path("accounts/", include("allauth.urls")),
# CKEditor 5

View file

@ -0,0 +1,36 @@
"Patienten-ID";"Vogel";"Alter";"Geschlecht";"Gefunden am";"Fundort";"Fundumstände";"Diagnose bei Fund";"Status";"Voliere";"Finder";"Benutzer";"Erstellt am";"Aktualisiert am";"Naturschutzbehörde";"Jagdbehörde";"Wildvogelhilfe-Team"
"FB001";"Test Bird 1749312567.204729";"adult";"unknown";"07.06.2025";"Test Location";"Test Circumstance";"Test finding";"Test bird status";"";"Vorname:
Nachname:
Straße:
Hausnummer:
Stadt:
PLZ:
Telefonnummer: ";"testuser1749312566.838063";"07.06.2025 16:09";"07.06.2025 16:09";"Ja";"Nein";"Ja"
"Angelo";"Mauersegler";"Nestling";"Männlich";"08.06.2025";"";"";"";"In Behandlung";"";"Vorname:
Nachname:
Straße:
Hausnummer:
Stadt:
PLZ:
Telefonnummer:";"admin";"08.06.2025 13:26";"08.06.2025 13:26";"Ja";"Nein";"Ja"
"Tim";"TestMelder";"Ei";"Männlich";"10.06.2025";"";"Verletzt";"";"In Behandlung";"";"Vorname:
Nachname:
Straße:
Hausnummer:
Stadt:
PLZ:
Telefonnummer:";"admin";"10.06.2025 06:23";"10.06.2025 06:23";"Ja";"Nein";"Ja"
"Theodore";"Turmfalke";"Nestling";"Weiblich";"10.06.2025";"Teststraße 123, Jena";"Test Circumstance";"Test - keine echte Verletzung";"In Behandlung";"";"Vorname: Max
Nachname: Mustermann
Straße:
Hausnummer:
Stadt:
PLZ:
Telefonnummer:";"admin";"10.06.2025 08:14";"10.06.2025 08:14";"Ja";"Ja";"Ja"
"Carlos";"Turmfalke";"Nestling";"Männlich";"10.06.2025";"";"";"";"In Behandlung";"";"Vorname:
Nachname:
Straße:
Hausnummer:
Stadt:
PLZ:
Telefonnummer:";"admin";"10.06.2025 08:18";"10.06.2025 08:18";"Ja";"Ja";"Ja"
1 Patienten-ID Vogel Alter Geschlecht Gefunden am Fundort Fundumstände Diagnose bei Fund Status Voliere Finder Benutzer Erstellt am Aktualisiert am Naturschutzbehörde Jagdbehörde Wildvogelhilfe-Team
2 FB001 Test Bird 1749312567.204729 adult unknown 07.06.2025 Test Location Test Circumstance Test finding Test bird status Vorname: Nachname: Straße: Hausnummer: Stadt: PLZ: Telefonnummer: testuser1749312566.838063 07.06.2025 16:09 07.06.2025 16:09 Ja Nein Ja
3 Angelo Mauersegler Nestling Männlich 08.06.2025 In Behandlung Vorname: Nachname: Straße: Hausnummer: Stadt: PLZ: Telefonnummer: admin 08.06.2025 13:26 08.06.2025 13:26 Ja Nein Ja
4 Tim TestMelder Ei Männlich 10.06.2025 Verletzt In Behandlung Vorname: Nachname: Straße: Hausnummer: Stadt: PLZ: Telefonnummer: admin 10.06.2025 06:23 10.06.2025 06:23 Ja Nein Ja
5 Theodore Turmfalke Nestling Weiblich 10.06.2025 Teststraße 123, Jena Test Circumstance Test - keine echte Verletzung In Behandlung Vorname: Max Nachname: Mustermann Straße: Hausnummer: Stadt: PLZ: Telefonnummer: admin 10.06.2025 08:14 10.06.2025 08:14 Ja Ja Ja
6 Carlos Turmfalke Nestling Männlich 10.06.2025 In Behandlung Vorname: Nachname: Straße: Hausnummer: Stadt: PLZ: Telefonnummer: admin 10.06.2025 08:18 10.06.2025 08:18 Ja Ja Ja

View file

@ -0,0 +1,36 @@
"Patienten-ID";"Vogel";"Alter";"Geschlecht";"Gefunden am";"Fundort";"Fundumstände";"Diagnose bei Fund";"Status";"Voliere";"Finder";"Benutzer";"Erstellt am";"Aktualisiert am";"Naturschutzbehörde";"Jagdbehörde";"Wildvogelhilfe-Team"
"FB001";"Test Bird 1749312567.204729";"adult";"unknown";"07.06.2025";"Test Location";"Test Circumstance";"Test finding";"Test bird status";"";"Vorname:
Nachname:
Straße:
Hausnummer:
Stadt:
PLZ:
Telefonnummer: ";"testuser1749312566.838063";"07.06.2025 16:09";"07.06.2025 16:09";"Ja";"Nein";"Ja"
"Angelo";"Mauersegler";"Nestling";"Männlich";"08.06.2025";"";"";"";"In Behandlung";"";"Vorname:
Nachname:
Straße:
Hausnummer:
Stadt:
PLZ:
Telefonnummer:";"admin";"08.06.2025 13:26";"08.06.2025 13:26";"Ja";"Nein";"Ja"
"Tim";"TestMelder";"Ei";"Männlich";"10.06.2025";"";"Verletzt";"";"In Behandlung";"";"Vorname:
Nachname:
Straße:
Hausnummer:
Stadt:
PLZ:
Telefonnummer:";"admin";"10.06.2025 06:23";"10.06.2025 06:23";"Ja";"Nein";"Ja"
"Theodore";"Turmfalke";"Nestling";"Weiblich";"10.06.2025";"Teststraße 123, Jena";"Test Circumstance";"Test - keine echte Verletzung";"In Behandlung";"";"Vorname: Max
Nachname: Mustermann
Straße:
Hausnummer:
Stadt:
PLZ:
Telefonnummer:";"admin";"10.06.2025 08:14";"10.06.2025 08:14";"Ja";"Ja";"Ja"
"Carlos";"Turmfalke";"Nestling";"Männlich";"10.06.2025";"";"";"";"In Behandlung";"";"Vorname:
Nachname:
Straße:
Hausnummer:
Stadt:
PLZ:
Telefonnummer:";"admin";"10.06.2025 08:18";"10.06.2025 08:18";"Ja";"Ja";"Ja"
1 Patienten-ID Vogel Alter Geschlecht Gefunden am Fundort Fundumstände Diagnose bei Fund Status Voliere Finder Benutzer Erstellt am Aktualisiert am Naturschutzbehörde Jagdbehörde Wildvogelhilfe-Team
2 FB001 Test Bird 1749312567.204729 adult unknown 07.06.2025 Test Location Test Circumstance Test finding Test bird status Vorname: Nachname: Straße: Hausnummer: Stadt: PLZ: Telefonnummer: testuser1749312566.838063 07.06.2025 16:09 07.06.2025 16:09 Ja Nein Ja
3 Angelo Mauersegler Nestling Männlich 08.06.2025 In Behandlung Vorname: Nachname: Straße: Hausnummer: Stadt: PLZ: Telefonnummer: admin 08.06.2025 13:26 08.06.2025 13:26 Ja Nein Ja
4 Tim TestMelder Ei Männlich 10.06.2025 Verletzt In Behandlung Vorname: Nachname: Straße: Hausnummer: Stadt: PLZ: Telefonnummer: admin 10.06.2025 06:23 10.06.2025 06:23 Ja Nein Ja
5 Theodore Turmfalke Nestling Weiblich 10.06.2025 Teststraße 123, Jena Test Circumstance Test - keine echte Verletzung In Behandlung Vorname: Max Nachname: Mustermann Straße: Hausnummer: Stadt: PLZ: Telefonnummer: admin 10.06.2025 08:14 10.06.2025 08:14 Ja Ja Ja
6 Carlos Turmfalke Nestling Männlich 10.06.2025 In Behandlung Vorname: Nachname: Straße: Hausnummer: Stadt: PLZ: Telefonnummer: admin 10.06.2025 08:18 10.06.2025 08:18 Ja Ja Ja

0
app/reports/__init__.py Normal file
View file

123
app/reports/admin.py Normal file
View file

@ -0,0 +1,123 @@
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from django.utils.html import format_html
from django.urls import reverse
from .models import AutomaticReport, ReportLog
@admin.register(AutomaticReport)
class AutomaticReportAdmin(admin.ModelAdmin):
list_display = [
'name',
'frequency',
'is_active',
'last_sent',
'created_by',
'created_at',
'email_count'
]
list_filter = ['frequency', 'is_active', 'created_at', 'include_naturschutzbehoerde', 'include_jagdbehoerde']
search_fields = ['name', 'description']
readonly_fields = ['created_by', 'created_at', 'updated_at']
fieldsets = (
(None, {
'fields': ('name', 'description', 'is_active')
}),
(_('E-Mail-Einstellungen'), {
'fields': ('email_addresses',)
}),
(_('Filter-Einstellungen'), {
'fields': ('include_naturschutzbehoerde', 'include_jagdbehoerde')
}),
(_('Zeitplan'), {
'fields': ('frequency',)
}),
(_('Metadaten'), {
'fields': ('created_by', 'created_at', 'updated_at', 'last_sent'),
'classes': ('collapse',)
}),
)
def save_model(self, request, obj, form, change):
if not change: # Creating new object
obj.created_by = request.user
super().save_model(request, obj, form, change)
def email_count(self, obj):
"""Show number of email addresses."""
count = obj.email_addresses.count()
return f"{count} E-Mail-Adresse(n)"
email_count.short_description = _("E-Mail-Adressen")
@admin.register(ReportLog)
class ReportLogAdmin(admin.ModelAdmin):
list_display = [
'created_at',
'get_report_type',
'date_range',
'patient_count',
'has_email_recipients',
'filters_used'
]
list_filter = [
'automatic_report',
'include_naturschutzbehörde',
'include_jagdbehörde',
'created_at'
]
search_fields = ['automatic_report__name']
readonly_fields = [
'automatic_report', 'date_from', 'date_to', 'include_naturschutzbehörde',
'include_jagdbehörde', 'patient_count', 'email_sent_to',
'created_at', 'csv_file'
]
def get_report_type(self, obj):
"""Show report type."""
if obj.automatic_report:
return format_html(
'<span style="color: #007bff;"><i class="fas fa-clock"></i> Automatisch</span>'
)
return format_html(
'<span style="color: #6c757d;"><i class="fas fa-hand-paper"></i> Manuell</span>'
)
get_report_type.short_description = _("Typ")
def date_range(self, obj):
"""Show date range."""
return f"{obj.date_from} - {obj.date_to}"
date_range.short_description = _("Zeitraum")
def has_email_recipients(self, obj):
"""Show if email was sent."""
if obj.email_sent_to:
return format_html(
'<span style="color: #28a745;"><i class="fas fa-envelope"></i> {} Empfänger</span>',
len(obj.email_sent_to)
)
return format_html(
'<span style="color: #17a2b8;"><i class="fas fa-download"></i> Download</span>'
)
has_email_recipients.short_description = _("Versendung")
def filters_used(self, obj):
"""Show which filters were used."""
filters = []
if obj.include_naturschutzbehörde:
filters.append("Naturschutzbehörde")
if obj.include_jagdbehörde:
filters.append("Jagdbehörde")
return ", ".join(filters) if filters else _("Keine Filter")
filters_used.short_description = _("Filter")
def has_add_permission(self, request):
"""Disable manual creation of logs."""
return False
# Custom admin site configuration
admin.site.site_header = "Django FBF Administration"
admin.site.site_title = "Django FBF Admin"
admin.site.index_title = "Willkommen zur Django FBF Administration"

8
app/reports/apps.py Normal file
View file

@ -0,0 +1,8 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class ReportsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "reports"
verbose_name = _("Reports")

157
app/reports/forms.py Normal file
View file

@ -0,0 +1,157 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from datetime import date, timedelta
from sendemail.models import Emailadress
from .models import AutomaticReport
class DateInput(forms.DateInput):
input_type = "date"
class ManualReportForm(forms.Form):
"""Form for creating manual reports."""
# Email selection
email_addresses = forms.ModelMultipleChoiceField(
queryset=Emailadress.objects.all(),
required=False,
widget=forms.CheckboxSelectMultiple,
label=_("E-Mail-Adressen"),
help_text=_("Wählen Sie E-Mail-Adressen aus oder lassen Sie das Feld leer für nur Download")
)
custom_email = forms.EmailField(
required=False,
label=_("Zusätzliche E-Mail-Adresse"),
help_text=_("Optional: Geben Sie eine zusätzliche E-Mail-Adresse ein")
)
# Date range
date_from = forms.DateField(
widget=DateInput(format="%Y-%m-%d"),
label=_("Von"),
initial=lambda: date.today() - timedelta(days=90), # 3 months ago
help_text=_("Startdatum für den Report")
)
date_to = forms.DateField(
widget=DateInput(format="%Y-%m-%d"),
label=_("Bis"),
initial=date.today,
help_text=_("Enddatum für den Report")
)
# Filter options
include_naturschutzbehoerde = forms.BooleanField(
required=False,
initial=True,
label=_("Naturschutzbehörde"),
help_text=_("Vögel einschließen, die an Naturschutzbehörde gemeldet werden")
)
include_jagdbehoerde = forms.BooleanField(
required=False,
initial=False,
label=_("Jagdbehörde"),
help_text=_("Vögel einschließen, die an Jagdbehörde gemeldet werden")
)
# Action choice
action_choices = [
('download', _('Nur herunterladen')),
('email', _('Per E-Mail senden')),
('both', _('Herunterladen und per E-Mail senden')),
]
action = forms.ChoiceField(
choices=action_choices,
widget=forms.RadioSelect,
initial='download',
label=_("Aktion")
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Set default date_from to 3 months ago
if not self.initial.get('date_from'):
self.fields['date_from'].initial = date.today() - timedelta(days=90)
def clean(self):
cleaned_data = super().clean()
date_from = cleaned_data.get('date_from')
date_to = cleaned_data.get('date_to')
action = cleaned_data.get('action')
email_addresses = cleaned_data.get('email_addresses')
custom_email = cleaned_data.get('custom_email')
# Validate date range
if date_from and date_to:
if date_from > date_to:
raise ValidationError(
_("Das 'Von'-Datum darf nicht nach dem 'Bis'-Datum liegen.")
)
# Validate email requirements for email actions
if action in ['email', 'both']:
if not email_addresses and not custom_email:
raise ValidationError(
_("Für E-Mail-Versendung müssen E-Mail-Adressen ausgewählt oder eingegeben werden.")
)
# Validate at least one filter is selected
include_naturschutz = cleaned_data.get('include_naturschutzbehoerde')
include_jagd = cleaned_data.get('include_jagdbehoerde')
if not include_naturschutz and not include_jagd:
raise ValidationError(
_("Mindestens eine Kategorie (Naturschutzbehörde oder Jagdbehörde) muss ausgewählt werden.")
)
return cleaned_data
class AutomaticReportForm(forms.ModelForm):
"""Form for creating/editing automatic reports."""
class Meta:
model = AutomaticReport
fields = [
'name',
'description',
'email_addresses',
'include_naturschutzbehoerde',
'include_jagdbehoerde',
'frequency',
'is_active'
]
widgets = {
'description': forms.Textarea(attrs={'rows': 3}),
'email_addresses': forms.CheckboxSelectMultiple,
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['email_addresses'].queryset = Emailadress.objects.all()
def clean(self):
cleaned_data = super().clean()
# Validate at least one filter is selected
include_naturschutz = cleaned_data.get('include_naturschutzbehoerde')
include_jagd = cleaned_data.get('include_jagdbehoerde')
if not include_naturschutz and not include_jagd:
raise ValidationError(
_("Mindestens eine Kategorie (Naturschutzbehörde oder Jagdbehörde) muss ausgewählt werden.")
)
# Validate email addresses are selected
email_addresses = cleaned_data.get('email_addresses')
if not email_addresses:
raise ValidationError(
_("Für automatische Reports müssen E-Mail-Adressen ausgewählt werden.")
)
return cleaned_data

View file

@ -0,0 +1 @@
# Empty file to make this directory a Python package

View file

@ -0,0 +1 @@
# Empty file to make this directory a Python package

View file

@ -0,0 +1,62 @@
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
from django.db import models
from datetime import date, timedelta
from bird.models import Bird, FallenBird, BirdStatus
from sendemail.models import Emailadress
from reports.models import AutomaticReport
class Command(BaseCommand):
help = 'Create test data for the reports system'
def handle(self, *args, **options):
self.stdout.write('Creating test data for reports system...')
# Create test email addresses
email1, created = Emailadress.objects.get_or_create(
email_address='test1@example.com',
defaults={'user_id': 1}
)
if created:
self.stdout.write(f'✓ Created email: {email1.email_address}')
email2, created = Emailadress.objects.get_or_create(
email_address='test2@example.com',
defaults={'user_id': 1}
)
if created:
self.stdout.write(f'✓ Created email: {email2.email_address}')
# Create a test automatic report
admin_user = User.objects.filter(is_superuser=True).first()
if admin_user:
auto_report, created = AutomaticReport.objects.get_or_create(
name='Test Weekly Report',
defaults={
'description': 'Automatic weekly report for testing',
'frequency': 'weekly',
'include_naturschutzbehoerde': True,
'include_jagdbehoerde': False,
'is_active': True,
'created_by': admin_user
}
)
if created:
auto_report.email_addresses.add(email1, email2)
self.stdout.write(f'✓ Created automatic report: {auto_report.name}')
# Check existing bird data
bird_count = FallenBird.objects.count()
self.stdout.write(f'✓ Found {bird_count} existing birds in database')
# Check birds with notification settings
notification_birds = Bird.objects.filter(
models.Q(melden_an_naturschutzbehoerde=True) |
models.Q(melden_an_jagdbehoerde=True)
).count()
self.stdout.write(f'✓ Found {notification_birds} birds with notification settings')
self.stdout.write(
self.style.SUCCESS('✓ Test data creation completed successfully!')
)

View file

@ -0,0 +1,129 @@
from django.core.management.base import BaseCommand
from django.utils import timezone
from datetime import date, timedelta
from reports.services import ReportGenerator
from reports.models import AutomaticReport
class Command(BaseCommand):
help = 'Test the report generation system'
def add_arguments(self, parser):
parser.add_argument(
'--test-manual',
action='store_true',
help='Test manual report generation',
)
parser.add_argument(
'--test-email',
action='store_true',
help='Test email sending (requires SMTP configuration)',
)
def handle(self, *args, **options):
self.stdout.write(self.style.SUCCESS('Testing Report System'))
# Simple test first
self.stdout.write('Reports app is working!')
# Test basic report generation
if options.get('test_manual'):
self.test_manual_report()
if options.get('test_email'):
self.test_email_report()
self.test_basic_functionality()
def test_basic_functionality(self):
self.stdout.write('Testing basic report functionality...')
# Create test date range (last 30 days)
date_to = date.today()
date_from = date_to - timedelta(days=30)
generator = ReportGenerator(
date_from=date_from,
date_to=date_to,
include_naturschutzbehoerde=True,
include_jagdbehoerde=False
)
# Test CSV generation
try:
csv_content, bird_count = generator.generate_csv()
self.stdout.write(
self.style.SUCCESS(
f'✓ CSV generation successful: {bird_count} birds found'
)
)
# Test summary
summary = generator.get_summary()
self.stdout.write(
self.style.SUCCESS(
f'✓ Summary generation successful: {summary["total_birds"]} total birds'
)
)
except Exception as e:
self.stdout.write(
self.style.ERROR(f'✗ CSV generation failed: {e}')
)
def test_manual_report(self):
self.stdout.write('Testing manual report creation...')
date_to = date.today()
date_from = date_to - timedelta(days=7) # Last week
generator = ReportGenerator(
date_from=date_from,
date_to=date_to,
include_naturschutzbehoerde=True,
include_jagdbehoerde=True
)
try:
# Create download log
log = generator.create_download_log()
self.stdout.write(
self.style.SUCCESS(
f'✓ Manual report log created: {log.id} with {log.patient_count} patients'
)
)
except Exception as e:
self.stdout.write(
self.style.ERROR(f'✗ Manual report creation failed: {e}')
)
def test_email_report(self):
self.stdout.write('Testing email report (dry run)...')
# This would test email functionality if SMTP is configured
# For now, just test the email template rendering
from django.template.loader import render_to_string
context = {
'date_from': '01.01.2024',
'date_to': '31.01.2024',
'patient_count': 42,
'filter_naturschutzbehörde': True,
'filter_jagdbehörde': False,
'automatic_report': None,
'created_at': '01.02.2024',
}
try:
subject = render_to_string('reports/email/report_subject.txt', context).strip()
message = render_to_string('reports/email/report_message.txt', context)
self.stdout.write(
self.style.SUCCESS(f'✓ Email template rendering successful')
)
self.stdout.write(f'Subject: {subject}')
except Exception as e:
self.stdout.write(
self.style.ERROR(f'✗ Email template rendering failed: {e}')
)

View file

@ -0,0 +1,62 @@
# Generated by Django 5.2.2 on 2025-06-10 09:05
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('sendemail', '0004_delete_birdemail'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='AutomaticReport',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Name')),
('description', models.TextField(blank=True, null=True, verbose_name='Beschreibung')),
('include_naturschutzbehoerde', models.BooleanField(default=True, help_text='Vögel einschließen, die an Naturschutzbehörde gemeldet werden', verbose_name='Naturschutzbehörde einschließen')),
('include_jagdbehoerde', models.BooleanField(default=False, help_text='Vögel einschließen, die an Jagdbehörde gemeldet werden', verbose_name='Jagdbehörde einschließen')),
('frequency', models.CharField(choices=[('weekly', 'Wöchentlich'), ('monthly', 'Monatlich'), ('quarterly', 'Vierteljährlich')], default='monthly', max_length=20, verbose_name='Häufigkeit')),
('is_active', models.BooleanField(default=True, verbose_name='Aktiv')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Aktualisiert am')),
('last_sent', models.DateTimeField(blank=True, null=True, verbose_name='Zuletzt gesendet')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Erstellt von')),
('email_addresses', models.ManyToManyField(help_text='E-Mail-Adressen, an die der Report gesendet wird', to='sendemail.emailadress', verbose_name='E-Mail-Adressen')),
],
options={
'verbose_name': 'Automatischer Report',
'verbose_name_plural': 'Automatische Reports',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='ReportLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('report_type', models.CharField(choices=[('manual', 'Manuell erstellt'), ('automatic', 'Automatisch erstellt')], max_length=20, verbose_name='Report-Typ')),
('date_from', models.DateField(verbose_name='Von')),
('date_to', models.DateField(verbose_name='Bis')),
('included_naturschutzbehoerde', models.BooleanField(verbose_name='Naturschutzbehörde eingeschlossen')),
('included_jagdbehoerde', models.BooleanField(verbose_name='Jagdbehörde eingeschlossen')),
('bird_count', models.IntegerField(verbose_name='Anzahl Vögel')),
('email_sent', models.BooleanField(default=False, verbose_name='E-Mail gesendet')),
('recipients', models.TextField(blank=True, verbose_name='Empfänger')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
('automatic_report', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='reports.automaticreport', verbose_name='Automatischer Report')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Erstellt von')),
],
options={
'verbose_name': 'Report-Log',
'verbose_name_plural': 'Report-Logs',
'ordering': ['-created_at'],
},
),
]

View file

@ -0,0 +1,58 @@
# Generated by Django 5.2.2 on 2025-06-10 09:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('reports', '0001_initial'),
]
operations = [
migrations.RenameField(
model_name='reportlog',
old_name='included_jagdbehoerde',
new_name='include_jagdbehörde',
),
migrations.RenameField(
model_name='reportlog',
old_name='included_naturschutzbehoerde',
new_name='include_naturschutzbehörde',
),
migrations.RemoveField(
model_name='reportlog',
name='bird_count',
),
migrations.RemoveField(
model_name='reportlog',
name='created_by',
),
migrations.RemoveField(
model_name='reportlog',
name='email_sent',
),
migrations.RemoveField(
model_name='reportlog',
name='recipients',
),
migrations.RemoveField(
model_name='reportlog',
name='report_type',
),
migrations.AddField(
model_name='reportlog',
name='csv_file',
field=models.FileField(blank=True, null=True, upload_to='reports/csv/', verbose_name='CSV-Datei'),
),
migrations.AddField(
model_name='reportlog',
name='email_sent_to',
field=models.JSONField(blank=True, default=list, help_text='Liste der E-Mail-Adressen, an die der Report gesendet wurde', verbose_name='E-Mail gesendet an'),
),
migrations.AddField(
model_name='reportlog',
name='patient_count',
field=models.IntegerField(default=0, verbose_name='Anzahl Patienten'),
),
]

View file

146
app/reports/models.py Normal file
View file

@ -0,0 +1,146 @@
from django.db import models
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from sendemail.models import Emailadress
class AutomaticReport(models.Model):
"""Model for automatic report configuration."""
name = models.CharField(
max_length=255,
verbose_name=_("Name")
)
description = models.TextField(
blank=True,
null=True,
verbose_name=_("Beschreibung")
)
# Email recipients
email_addresses = models.ManyToManyField(
Emailadress,
verbose_name=_("E-Mail-Adressen"),
help_text=_("E-Mail-Adressen, an die der Report gesendet wird")
)
# Report filters
include_naturschutzbehoerde = models.BooleanField(
default=True,
verbose_name=_("Naturschutzbehörde einschließen"),
help_text=_("Vögel einschließen, die an Naturschutzbehörde gemeldet werden")
)
include_jagdbehoerde = models.BooleanField(
default=False,
verbose_name=_("Jagdbehörde einschließen"),
help_text=_("Vögel einschließen, die an Jagdbehörde gemeldet werden")
)
# Schedule settings
frequency_choices = [
('weekly', _('Wöchentlich')),
('monthly', _('Monatlich')),
('quarterly', _('Vierteljährlich')),
]
frequency = models.CharField(
max_length=20,
choices=frequency_choices,
default='monthly',
verbose_name=_("Häufigkeit")
)
# Status
is_active = models.BooleanField(
default=True,
verbose_name=_("Aktiv")
)
# Metadata
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
verbose_name=_("Erstellt von")
)
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name=_("Erstellt am")
)
updated_at = models.DateTimeField(
auto_now=True,
verbose_name=_("Aktualisiert am")
)
last_sent = models.DateTimeField(
null=True,
blank=True,
verbose_name=_("Zuletzt gesendet")
)
def __str__(self):
return self.name
class Meta:
verbose_name = _("Automatischer Report")
verbose_name_plural = _("Automatische Reports")
ordering = ['-created_at']
class ReportLog(models.Model):
"""Log for generated reports."""
# Link to automatic report if applicable
automatic_report = models.ForeignKey(
AutomaticReport,
on_delete=models.CASCADE,
null=True,
blank=True,
verbose_name=_("Automatischer Report")
)
# Date range
date_from = models.DateField(verbose_name=_("Von"))
date_to = models.DateField(verbose_name=_("Bis"))
# Filters used
include_naturschutzbehörde = models.BooleanField(
verbose_name=_("Naturschutzbehörde eingeschlossen")
)
include_jagdbehörde = models.BooleanField(
verbose_name=_("Jagdbehörde eingeschlossen")
)
# Results
patient_count = models.IntegerField(
default=0,
verbose_name=_("Anzahl Patienten")
)
# Email info - stores list of email addresses as JSON
email_sent_to = models.JSONField(
default=list,
blank=True,
verbose_name=_("E-Mail gesendet an"),
help_text=_("Liste der E-Mail-Adressen, an die der Report gesendet wurde")
)
# CSV file storage
csv_file = models.FileField(
upload_to='reports/csv/',
null=True,
blank=True,
verbose_name=_("CSV-Datei")
)
# Metadata
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name=_("Erstellt am")
)
def __str__(self):
if self.automatic_report:
return f"Report {self.date_from} - {self.date_to} ({self.automatic_report.name})"
return f"Report {self.date_from} - {self.date_to} (Manuell)"
class Meta:
verbose_name = _("Report-Log")
verbose_name_plural = _("Report-Logs")
ordering = ['-created_at']

187
app/reports/services.py Normal file
View file

@ -0,0 +1,187 @@
import csv
from io import StringIO
from datetime import date
from django.db.models import Q
from django.core.mail import EmailMessage
from django.template.loader import render_to_string
from django.conf import settings
from django.core.files.base import ContentFile
from bird.models import FallenBird
class ReportGenerator:
"""Service class for generating bird reports."""
def __init__(self, date_from, date_to, include_naturschutzbehoerde=True, include_jagdbehoerde=False):
self.date_from = date_from
self.date_to = date_to
self.include_naturschutzbehoerde = include_naturschutzbehoerde
self.include_jagdbehoerde = include_jagdbehoerde
def get_birds_queryset(self):
"""Get queryset of birds based on filters."""
# Date filter
queryset = FallenBird.objects.filter(
date_found__gte=self.date_from,
date_found__lte=self.date_to
)
# Bird type filter based on notification settings
bird_filter = Q()
if self.include_naturschutzbehoerde:
bird_filter |= Q(bird__melden_an_naturschutzbehoerde=True)
if self.include_jagdbehoerde:
bird_filter |= Q(bird__melden_an_jagdbehoerde=True)
if bird_filter:
queryset = queryset.filter(bird_filter)
return queryset.select_related('bird', 'status', 'aviary', 'user').order_by('date_found')
def generate_csv(self):
"""Generate CSV content and return as string with bird count."""
birds = self.get_birds_queryset()
bird_count = birds.count()
# Create CSV in memory
output = StringIO()
writer = csv.writer(output, delimiter=';', quoting=csv.QUOTE_ALL)
# Header row
headers = [
'Vogel',
'Alter',
'Geschlecht',
'Gefunden am',
'Fundort',
'Fundumstände',
'Diagnose bei Fund',
'Status'
]
writer.writerow(headers)
# Data rows
for bird in birds:
row = [
bird.bird.name if bird.bird else '',
bird.get_age_display() if bird.age else '',
bird.get_sex_display() if bird.sex else '',
bird.date_found.strftime('%d.%m.%Y') if bird.date_found else '',
bird.place or '',
bird.find_circumstances or '',
bird.diagnostic_finding or '',
bird.status.description if bird.status else '',
]
writer.writerow(row)
csv_content = output.getvalue()
output.close()
return csv_content, bird_count
def get_filename(self):
"""Generate filename for the report."""
return f"wildvogelhilfe_report_{self.date_from}_{self.date_to}.csv"
def get_summary(self):
"""Get summary statistics for the report."""
birds = self.get_birds_queryset()
summary = {
'total_birds': birds.count(),
'naturschutz_birds': birds.filter(bird__melden_an_naturschutzbehoerde=True).count() if self.include_naturschutzbehoerde else 0,
'jagd_birds': birds.filter(bird__melden_an_jagdbehoerde=True).count() if self.include_jagdbehoerde else 0,
'date_from': self.date_from,
'date_to': self.date_to,
}
return summary
def send_email_report(self, email_addresses, automatic_report=None):
"""Send the report via email to specified addresses."""
from .models import ReportLog
csv_content, bird_count = self.generate_csv()
filename = self.get_filename()
# Prepare email context
context = {
'date_from': self.date_from.strftime('%d.%m.%Y'),
'date_to': self.date_to.strftime('%d.%m.%Y'),
'patient_count': bird_count,
'filter_naturschutzbehörde': self.include_naturschutzbehoerde,
'filter_jagdbehörde': self.include_jagdbehoerde,
'automatic_report': automatic_report,
'created_at': date.today().strftime('%d.%m.%Y'),
}
# Render email templates
subject = render_to_string('reports/email/report_subject.txt', context).strip()
message = render_to_string('reports/email/report_message.txt', context)
# Create email
email = EmailMessage(
subject=subject,
body=message,
from_email=getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@wildvogelhilfe-jena.de'),
to=email_addresses,
)
# Attach CSV file
email.attach(filename, csv_content, 'text/csv')
try:
# Send email
email.send()
# Create log entry
report_log = ReportLog.objects.create(
automatic_report=automatic_report,
date_from=self.date_from,
date_to=self.date_to,
patient_count=bird_count,
include_naturschutzbehörde=self.include_naturschutzbehoerde,
include_jagdbehörde=self.include_jagdbehoerde,
email_sent_to=email_addresses,
)
# Save CSV file to the log
report_log.csv_file.save(
filename,
ContentFile(csv_content.encode('utf-8')),
save=True
)
return report_log, True, None
except Exception as e:
return None, False, str(e)
def create_download_log(self, automatic_report=None):
"""Create a log entry for downloaded reports."""
from .models import ReportLog
csv_content, bird_count = self.generate_csv()
filename = self.get_filename()
# Create log entry
report_log = ReportLog.objects.create(
automatic_report=automatic_report,
date_from=self.date_from,
date_to=self.date_to,
patient_count=bird_count,
include_naturschutzbehörde=self.include_naturschutzbehoerde,
include_jagdbehörde=self.include_jagdbehoerde,
email_sent_to=[], # Empty list indicates download
)
# Save CSV file to the log
report_log.csv_file.save(
filename,
ContentFile(csv_content.encode('utf-8')),
save=True
)
return report_log

3
app/reports/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

14
app/reports/urls.py Normal file
View file

@ -0,0 +1,14 @@
from django.urls import path
from . import views
app_name = 'reports'
urlpatterns = [
path('', views.reports_dashboard, name='dashboard'),
path('manual/', views.manual_report, name='manual_report'),
path('automatic/', views.automatic_reports, name='automatic_reports'),
path('automatic/create/', views.create_automatic_report, name='create_automatic_report'),
path('automatic/<int:report_id>/edit/', views.edit_automatic_report, name='edit_automatic_report'),
path('automatic/<int:report_id>/delete/', views.delete_automatic_report, name='delete_automatic_report'),
path('logs/', views.report_logs, name='report_logs'),
]

178
app/reports/views.py Normal file
View file

@ -0,0 +1,178 @@
import csv
from datetime import date, timedelta
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib import messages
from django.http import HttpResponse
from django.core.mail import EmailMessage
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.template.loader import render_to_string
from django.db.models import Q
from bird.models import FallenBird
from .forms import ManualReportForm, AutomaticReportForm
from .models import AutomaticReport, ReportLog
from .services import ReportGenerator
@staff_member_required
def reports_dashboard(request):
"""Main reports dashboard."""
context = {
'title': 'Reports Dashboard',
}
return render(request, 'admin/reports/dashboard.html', context)
@staff_member_required
def manual_report(request):
"""Create and send/download manual reports."""
if request.method == 'POST':
form = ManualReportForm(request.POST)
if form.is_valid():
# Handle form submission based on action
action = request.POST.get('action')
if action == 'download':
# Generate CSV and return as download
generator = ReportGenerator(
date_from=form.cleaned_data['date_from'],
date_to=form.cleaned_data['date_to'],
include_naturschutzbehoerde=form.cleaned_data['include_naturschutzbehörde'],
include_jagdbehoerde=form.cleaned_data['include_jagdbehörde']
)
csv_content, bird_count = generator.generate_csv()
filename = generator.get_filename()
# Create download log
generator.create_download_log()
response = HttpResponse(csv_content, content_type='text/csv')
response['Content-Disposition'] = f'attachment; filename="{filename}"'
messages.success(request, f'Report mit {bird_count} Patienten wurde heruntergeladen.')
return response
elif action == 'email':
# Send via email
email_addresses = form.cleaned_data['email_addresses']
email_list = [email.email for email in email_addresses]
# Add custom email if provided
if form.cleaned_data.get('custom_email'):
email_list.append(form.cleaned_data['custom_email'])
if not email_list:
messages.error(request, 'Bitte wählen Sie mindestens eine E-Mail-Adresse aus.')
return render(request, 'admin/reports/manual_report.html', {'form': form, 'title': 'Manuellen Report erstellen'})
generator = ReportGenerator(
date_from=form.cleaned_data['date_from'],
date_to=form.cleaned_data['date_to'],
include_naturschutzbehoerde=form.cleaned_data['include_naturschutzbehörde'],
include_jagdbehoerde=form.cleaned_data['include_jagdbehörde']
)
report_log, success, error = generator.send_email_report(email_list)
if success:
messages.success(
request,
f'Report wurde erfolgreich an {len(email_list)} E-Mail-Adresse(n) gesendet.'
)
return redirect('reports:dashboard')
else:
messages.error(request, f'Fehler beim Senden des Reports: {error}')
else:
form = ManualReportForm()
context = {
'form': form,
'title': 'Manuellen Report erstellen',
}
return render(request, 'admin/reports/manual_report.html', context)
@staff_member_required
def automatic_reports(request):
"""List and manage automatic reports."""
reports = AutomaticReport.objects.all()
context = {
'reports': reports,
'title': 'Automatische Reports',
}
return render(request, 'admin/reports/automatic_reports.html', context)
@staff_member_required
def create_automatic_report(request):
"""Create new automatic report."""
if request.method == 'POST':
form = AutomaticReportForm(request.POST)
if form.is_valid():
report = form.save(commit=False)
report.created_by = request.user
report.save()
form.save_m2m() # Save many-to-many relationships
messages.success(request, 'Automatischer Report wurde erfolgreich erstellt.')
return redirect('reports:automatic_reports')
else:
form = AutomaticReportForm()
context = {
'form': form,
'title': 'Automatischen Report erstellen',
}
return render(request, 'admin/reports/automatic_report_form.html', context)
@staff_member_required
def edit_automatic_report(request, report_id):
"""Edit automatic report."""
report = get_object_or_404(AutomaticReport, id=report_id)
if request.method == 'POST':
form = AutomaticReportForm(request.POST, instance=report)
if form.is_valid():
form.save()
messages.success(request, 'Automatischer Report wurde erfolgreich aktualisiert.')
return redirect('reports:automatic_reports')
else:
form = AutomaticReportForm(instance=report)
context = {
'form': form,
'report': report,
'title': f'Report bearbeiten: {report.name}',
}
return render(request, 'admin/reports/automatic_report_form.html', context)
@staff_member_required
def delete_automatic_report(request, report_id):
"""Delete automatic report."""
report = get_object_or_404(AutomaticReport, id=report_id)
if request.method == 'POST':
report.delete()
messages.success(request, 'Automatischer Report wurde erfolgreich gelöscht.')
return redirect('reports:automatic_reports')
context = {
'report': report,
'title': f'Report löschen: {report.name}',
}
return render(request, 'admin/reports/automatic_report_confirm_delete.html', context)
@staff_member_required
def report_logs(request):
"""View report logs."""
logs = ReportLog.objects.all().order_by('-created_at')[:100] # Show last 100 logs
context = {
'report_logs': logs,
'title': 'Report-Protokoll',
}
return render(request, 'admin/reports/report_logs.html', context)

View file

@ -0,0 +1,104 @@
{% extends "admin/reports/base.html" %}
{% load i18n %}
{% block title %}{% trans "Automatischen Report löschen" %}{% endblock %}
{% block content %}
<div class="report-content">
<div class="card">
<div class="card-header">
<h3>{% trans "Automatischen Report löschen" %}</h3>
</div>
<div class="card-body">
<div class="alert alert-warning">
<strong>{% trans "Achtung!" %}</strong>
{% trans "Sind Sie sicher, dass Sie den automatischen Report" %}
<strong>"{{ object.name }}"</strong>
{% trans "löschen möchten?" %}
</div>
<div class="report-details">
<h4>{% trans "Report-Details:" %}</h4>
<ul>
<li><strong>{% trans "Name:" %}</strong> {{ object.name }}</li>
<li><strong>{% trans "Häufigkeit:" %}</strong> {{ object.get_frequency_display }}</li>
<li><strong>{% trans "Status:" %}</strong>
{% if object.is_active %}
<span class="badge badge-success">{% trans "Aktiv" %}</span>
{% else %}
<span class="badge badge-secondary">{% trans "Inaktiv" %}</span>
{% endif %}
</li>
<li><strong>{% trans "Erstellt am:" %}</strong> {{ object.created_at|date:"d.m.Y H:i" }}</li>
<li><strong>{% trans "E-Mail-Adressen:" %}</strong>
<ul>
{% for email in object.email_addresses.all %}
<li>{{ email.email }}</li>
{% endfor %}
</ul>
</li>
</ul>
</div>
<div class="alert alert-info">
<strong>{% trans "Hinweis:" %}</strong>
{% trans "Das Löschen des automatischen Reports stoppt alle zukünftigen automatischen Versendungen. Bereits gesendete Reports bleiben im Report-Log erhalten." %}
</div>
<form method="post" class="delete-form">
{% csrf_token %}
<div class="form-actions">
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash"></i> {% trans "Ja, löschen" %}
</button>
<a href="{% url 'reports:automatic_reports' %}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> {% trans "Abbrechen" %}
</a>
</div>
</form>
</div>
</div>
</div>
<style>
.report-details {
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin: 15px 0;
}
.report-details ul {
margin-bottom: 0;
}
.report-details ul li {
margin-bottom: 8px;
}
.report-details ul ul {
margin-top: 5px;
margin-left: 20px;
}
.delete-form {
margin-top: 20px;
}
.badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 0.875em;
}
.badge-success {
background-color: #28a745;
color: white;
}
.badge-secondary {
background-color: #6c757d;
color: white;
}
</style>
{% endblock %}

View file

@ -0,0 +1,145 @@
{% extends "admin/reports/base.html" %}
{% load i18n %}
{% block title %}
{% if object %}
{% trans "Automatischen Report bearbeiten" %}
{% else %}
{% trans "Automatischen Report erstellen" %}
{% endif %}
{% endblock %}
{% block content %}
<div class="report-content">
<div class="card">
<div class="card-header">
<h3>
{% if object %}
{% trans "Automatischen Report bearbeiten" %}
{% else %}
{% trans "Automatischen Report erstellen" %}
{% endif %}
</h3>
</div>
<div class="card-body">
<form method="post" class="report-form">
{% csrf_token %}
<div class="form-row">
<div class="form-group">
<label for="{{ form.name.id_for_label }}">{{ form.name.label }}</label>
{{ form.name }}
{% if form.name.help_text %}
<small class="form-text text-muted">{{ form.name.help_text }}</small>
{% endif %}
{% if form.name.errors %}
<div class="error-messages">
{% for error in form.name.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
</div>
{% endif %}
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="{{ form.email_addresses.id_for_label }}">{{ form.email_addresses.label }}</label>
{{ form.email_addresses }}
{% if form.email_addresses.help_text %}
<small class="form-text text-muted">{{ form.email_addresses.help_text }}</small>
{% endif %}
{% if form.email_addresses.errors %}
<div class="error-messages">
{% for error in form.email_addresses.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
</div>
{% endif %}
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="{{ form.frequency.id_for_label }}">{{ form.frequency.label }}</label>
{{ form.frequency }}
{% if form.frequency.errors %}
<div class="error-messages">
{% for error in form.frequency.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
</div>
{% endif %}
</div>
<div class="form-group col-md-6">
<label for="{{ form.is_active.id_for_label }}">{{ form.is_active.label }}</label>
<div class="checkbox-wrapper">
{{ form.is_active }}
</div>
{% if form.is_active.errors %}
<div class="error-messages">
{% for error in form.is_active.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
</div>
{% endif %}
</div>
</div>
<div class="form-section">
<h4>{% trans "Filter-Optionen" %}</h4>
<div class="form-row">
<div class="form-group col-md-6">
<div class="checkbox-wrapper">
{{ form.include_naturschutzbehörde }}
<label for="{{ form.include_naturschutzbehörde.id_for_label }}">{{ form.include_naturschutzbehörde.label }}</label>
</div>
{% if form.include_naturschutzbehörde.errors %}
<div class="error-messages">
{% for error in form.include_naturschutzbehörde.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
</div>
{% endif %}
</div>
<div class="form-group col-md-6">
<div class="checkbox-wrapper">
{{ form.include_jagdbehörde }}
<label for="{{ form.include_jagdbehörde.id_for_label }}">{{ form.include_jagdbehörde.label }}</label>
</div>
{% if form.include_jagdbehörde.errors %}
<div class="error-messages">
{% for error in form.include_jagdbehörde.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
{% if form.non_field_errors %}
<div class="error-messages">
{% for error in form.non_field_errors %}
<div class="error">{{ error }}</div>
{% endfor %}
</div>
{% endif %}
<div class="form-actions">
<button type="submit" class="btn btn-primary">
{% if object %}
{% trans "Speichern" %}
{% else %}
{% trans "Erstellen" %}
{% endif %}
</button>
<a href="{% url 'reports:automatic_reports' %}" class="btn btn-secondary">
{% trans "Abbrechen" %}
</a>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,157 @@
{% extends "admin/reports/base.html" %}
{% load i18n %}
{% block reports_content %}
<div class="automatic-reports">
<h1>⚙️ Automatische Reports</h1>
<p>Verwalten Sie automatische Reports, die regelmäßig versendet werden.</p>
<div class="actions">
<a href="{% url 'reports:create_automatic_report' %}" class="btn btn-primary"> Neuen automatischen Report erstellen</a>
</div>
{% if reports %}
<div class="reports-table">
<table>
<thead>
<tr>
<th>Name</th>
<th>Häufigkeit</th>
<th>Filter</th>
<th>E-Mail-Adressen</th>
<th>Status</th>
<th>Zuletzt gesendet</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for report in reports %}
<tr>
<td>
<strong>{{ report.name }}</strong>
{% if report.description %}<br><small>{{ report.description|truncatechars:50 }}</small>{% endif %}
</td>
<td>{{ report.get_frequency_display }}</td>
<td>
{% if report.include_naturschutzbehoerde %}
<span class="badge badge-naturschutz">Naturschutzbehörde</span>
{% endif %}
{% if report.include_jagdbehoerde %}
<span class="badge badge-jagd">Jagdbehörde</span>
{% endif %}
</td>
<td>{{ report.email_addresses.count }} Adresse(n)</td>
<td>
{% if report.is_active %}
<span class="status status-active">✅ Aktiv</span>
{% else %}
<span class="status status-inactive">❌ Inaktiv</span>
{% endif %}
</td>
<td>
{% if report.last_sent %}
{{ report.last_sent|date:"d.m.Y H:i" }}
{% else %}
<em>Noch nie gesendet</em>
{% endif %}
</td>
<td class="actions-cell">
<a href="{% url 'reports:edit_automatic_report' report.id %}" class="btn btn-small btn-edit">✏️ Bearbeiten</a>
<a href="{% url 'reports:delete_automatic_report' report.id %}" class="btn btn-small btn-delete">🗑️ Löschen</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-state">
<h3>Keine automatischen Reports konfiguriert</h3>
<p>Sie haben noch keine automatischen Reports erstellt. Klicken Sie auf den Button oben, um Ihren ersten automatischen Report zu erstellen.</p>
</div>
{% endif %}
</div>
<style>
.actions {
margin-bottom: 30px;
}
.reports-table {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.reports-table table {
width: 100%;
border-collapse: collapse;
}
.reports-table th {
background: #417690;
color: white;
padding: 15px;
text-align: left;
font-weight: bold;
}
.reports-table td {
padding: 15px;
border-bottom: 1px solid #eee;
vertical-align: top;
}
.reports-table tr:hover {
background: #f8f9fa;
}
.badge {
display: inline-block;
padding: 4px 8px;
border-radius: 12px;
font-size: 0.8em;
font-weight: bold;
margin: 2px;
}
.badge-naturschutz {
background: #e8f5e8;
color: #2e7d32;
}
.badge-jagd {
background: #fff3e0;
color: #f57c00;
}
.status {
font-weight: bold;
}
.status-active {
color: #2e7d32;
}
.status-inactive {
color: #d32f2f;
}
.actions-cell {
white-space: nowrap;
}
.btn-small {
padding: 6px 12px;
font-size: 0.9em;
margin: 2px;
}
.btn-edit {
background: #1976d2;
color: white;
}
.btn-delete {
background: #d32f2f;
color: white;
}
.empty-state {
text-align: center;
padding: 60px 20px;
background: white;
border-radius: 8px;
color: #666;
}
.empty-state h3 {
color: #417690;
margin-bottom: 15px;
}
</style>
{% endblock %}

View file

@ -0,0 +1,72 @@
{% extends "admin/base_site.html" %}
{% load i18n admin_urls static admin_list %}
{% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">Django FBF Administration</a></h1>
{% endblock %}
{% block nav-global %}
<div class="nav-global">
<a href="{% url 'reports:dashboard' %}" class="{% if request.resolver_match.url_name == 'dashboard' %}current{% endif %}">📊 Dashboard</a>
<a href="{% url 'reports:manual_report' %}" class="{% if request.resolver_match.url_name == 'manual_report' %}current{% endif %}">📝 Report erstellen</a>
<a href="{% url 'reports:automatic_reports' %}" class="{% if request.resolver_match.url_name == 'automatic_reports' %}current{% endif %}">⚙️ Automatischer Report</a>
<a href="{% url 'reports:report_logs' %}" class="{% if request.resolver_match.url_name == 'report_logs' %}current{% endif %}">📋 Protokoll</a>
</div>
{% endblock %}
{% block content %}
<div class="reports-content">
{% if messages %}
<div class="messagelist">
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% block reports_content %}
{% endblock %}
</div>
{% endblock %}
{% block extrahead %}
{{ block.super }}
<style>
.nav-global {
background: #417690;
padding: 10px 20px;
margin: 0 -20px 20px -20px;
}
.nav-global a {
color: white;
text-decoration: none;
margin-right: 20px;
padding: 8px 12px;
border-radius: 4px;
}
.nav-global a:hover, .nav-global a.current {
background: rgba(255,255,255,0.2);
}
.reports-content {
max-width: 1200px;
}
.alert {
padding: 12px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
}
.alert-success {
color: #155724;
background-color: #d4edda;
border-color: #c3e6cb;
}
.alert-error {
color: #721c24;
background-color: #f8d7da;
border-color: #f5c6cb;
}
</style>
{% endblock %}

View file

@ -0,0 +1,85 @@
{% extends "admin/reports/base.html" %}
{% load i18n %}
{% block reports_content %}
<div class="dashboard">
<h1>📊 Reports Dashboard</h1>
<p>Willkommen zum Reports-System der Django FBF Anwendung. Hier können Sie Berichte über gefallene Vögel erstellen und verwalten.</p>
<div class="dashboard-modules">
<div class="module">
<h2>📝 Report erstellen</h2>
<p>Erstellen Sie sofort einen benutzerdefinierten Report für einen bestimmten Zeitraum.</p>
<ul>
<li>Zeitraum auswählen (Standard: letzte 3 Monate)</li>
<li>Filter nach Naturschutzbehörde/Jagdbehörde</li>
<li>E-Mail versenden oder als CSV herunterladen</li>
</ul>
<a href="{% url 'reports:manual_report' %}" class="btn btn-primary">Report erstellen</a>
</div>
<div class="module">
<h2>⚙️ Automatischer Report</h2>
<p>Konfigurieren Sie automatische Reports, die regelmäßig versendet werden.</p>
<ul>
<li>Wöchentliche, monatliche oder vierteljährliche Reports</li>
<li>Vordefinierte E-Mail-Empfänger</li>
<li>Automatische Zeitraum-Berechnung</li>
</ul>
<a href="{% url 'reports:automatic_reports' %}" class="btn btn-primary">Automatische Reports verwalten</a>
</div>
<div class="module">
<h2>📋 Report-Protokoll</h2>
<p>Übersicht über alle erstellten und versendeten Reports.</p>
<ul>
<li>Historie aller Reports</li>
<li>Status der E-Mail-Versendung</li>
<li>Filter und Statistiken</li>
</ul>
<a href="{% url 'reports:report_logs' %}" class="btn btn-primary">Protokoll anzeigen</a>
</div>
</div>
</div>
<style>
.dashboard-modules {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 30px;
}
.module {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.module h2 {
color: #417690;
margin-top: 0;
}
.module ul {
margin: 15px 0;
padding-left: 20px;
}
.module li {
margin-bottom: 8px;
color: #666;
}
.btn {
display: inline-block;
padding: 10px 20px;
background: #417690;
color: white;
text-decoration: none;
border-radius: 4px;
margin-top: 15px;
}
.btn:hover {
background: #2e5a70;
color: white;
}
</style>
{% endblock %}

View file

@ -0,0 +1,178 @@
{% extends "admin/reports/base.html" %}
{% load i18n crispy_forms_tags %}
{% block reports_content %}
<div class="manual-report">
<h1>📝 Report erstellen</h1>
<p>Erstellen Sie einen benutzerdefinierten Report für einen bestimmten Zeitraum.</p>
<form method="post" class="report-form">
{% csrf_token %}
<fieldset class="fieldset">
<legend>📅 Zeitraum</legend>
<div class="form-row">
<div class="form-group">
<label for="{{ form.date_from.id_for_label }}">{{ form.date_from.label }}</label>
{{ form.date_from }}
{% if form.date_from.help_text %}<p class="help">{{ form.date_from.help_text }}</p>{% endif %}
{% if form.date_from.errors %}<ul class="errorlist">{% for error in form.date_from.errors %}<li>{{ error }}</li>{% endfor %}</ul>{% endif %}
</div>
<div class="form-group">
<label for="{{ form.date_to.id_for_label }}">{{ form.date_to.label }}</label>
{{ form.date_to }}
{% if form.date_to.help_text %}<p class="help">{{ form.date_to.help_text }}</p>{% endif %}
{% if form.date_to.errors %}<ul class="errorlist">{% for error in form.date_to.errors %}<li>{{ error }}</li>{% endfor %}</ul>{% endif %}
</div>
</div>
</fieldset>
<fieldset class="fieldset">
<legend>🎯 Filter</legend>
<div class="form-row">
<div class="form-group checkbox-group">
{{ form.include_naturschutzbehoerde }}
<label for="{{ form.include_naturschutzbehoerde.id_for_label }}">{{ form.include_naturschutzbehoerde.label }}</label>
{% if form.include_naturschutzbehoerde.help_text %}<p class="help">{{ form.include_naturschutzbehoerde.help_text }}</p>{% endif %}
</div>
<div class="form-group checkbox-group">
{{ form.include_jagdbehoerde }}
<label for="{{ form.include_jagdbehoerde.id_for_label }}">{{ form.include_jagdbehoerde.label }}</label>
{% if form.include_jagdbehoerde.help_text %}<p class="help">{{ form.include_jagdbehoerde.help_text }}</p>{% endif %}
</div>
</div>
{% if form.include_naturschutzbehoerde.errors or form.include_jagdbehoerde.errors %}
<ul class="errorlist">
{% for error in form.include_naturschutzbehoerde.errors %}<li>{{ error }}</li>{% endfor %}
{% for error in form.include_jagdbehoerde.errors %}<li>{{ error }}</li>{% endfor %}
</ul>
{% endif %}
</fieldset>
<fieldset class="fieldset">
<legend>📧 E-Mail-Adressen</legend>
<div class="form-group">
<label>{{ form.email_addresses.label }}</label>
{{ form.email_addresses }}
{% if form.email_addresses.help_text %}<p class="help">{{ form.email_addresses.help_text }}</p>{% endif %}
{% if form.email_addresses.errors %}<ul class="errorlist">{% for error in form.email_addresses.errors %}<li>{{ error }}</li>{% endfor %}</ul>{% endif %}
</div>
<div class="form-group">
<label for="{{ form.custom_email.id_for_label }}">{{ form.custom_email.label }}</label>
{{ form.custom_email }}
{% if form.custom_email.help_text %}<p class="help">{{ form.custom_email.help_text }}</p>{% endif %}
{% if form.custom_email.errors %}<ul class="errorlist">{% for error in form.custom_email.errors %}<li>{{ error }}</li>{% endfor %}</ul>{% endif %}
</div>
</fieldset>
<fieldset class="fieldset">
<legend>🎬 Aktion</legend>
<div class="form-group">
{{ form.action }}
{% if form.action.errors %}<ul class="errorlist">{% for error in form.action.errors %}<li>{{ error }}</li>{% endfor %}</ul>{% endif %}
</div>
</fieldset>
{% if form.non_field_errors %}
<div class="form-errors">
{{ form.non_field_errors }}
</div>
{% endif %}
<div class="form-actions">
<button type="submit" class="btn btn-primary">Report erstellen</button>
<a href="{% url 'reports:dashboard' %}" class="btn btn-secondary">Abbrechen</a>
</div>
</form>
</div>
<style>
.report-form {
max-width: 800px;
}
.fieldset {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
background: white;
}
.fieldset legend {
font-weight: bold;
color: #417690;
padding: 0 10px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
font-weight: bold;
margin-bottom: 5px;
color: #333;
}
.form-group input, .form-group select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 8px;
}
.checkbox-group input {
width: auto;
}
.checkbox-group label {
margin-bottom: 0;
}
.help {
font-size: 0.9em;
color: #666;
margin-top: 5px;
}
.errorlist {
color: #d32f2f;
list-style: none;
padding: 0;
margin: 5px 0;
}
.form-errors {
background: #ffebee;
border: 1px solid #f5c6cb;
border-radius: 4px;
padding: 10px;
margin-bottom: 20px;
}
.form-actions {
margin-top: 30px;
}
.btn {
display: inline-block;
padding: 10px 20px;
border: none;
border-radius: 4px;
text-decoration: none;
cursor: pointer;
margin-right: 10px;
}
.btn-primary {
background: #417690;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn:hover {
opacity: 0.9;
}
</style>
{% endblock %}

View file

@ -0,0 +1,278 @@
{% extends "admin/reports/base.html" %}
{% load i18n %}
{% block title %}{% trans "Report-Protokoll" %}{% endblock %}
{% block content %}
<div class="report-content">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h3>{% trans "Report-Protokoll" %}</h3>
<div class="header-actions">
<span class="badge badge-info">{{ report_logs|length }} {% trans "Einträge" %}</span>
</div>
</div>
<div class="card-body">
{% if report_logs %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-light">
<tr>
<th>{% trans "Datum/Zeit" %}</th>
<th>{% trans "Typ" %}</th>
<th>{% trans "Name" %}</th>
<th>{% trans "Zeitraum" %}</th>
<th>{% trans "Anzahl Patienten" %}</th>
<th>{% trans "E-Mail gesendet an" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Aktionen" %}</th>
</tr>
</thead>
<tbody>
{% for log in report_logs %}
<tr>
<td>
<div class="datetime-cell">
<div class="date">{{ log.created_at|date:"d.m.Y" }}</div>
<div class="time text-muted">{{ log.created_at|time:"H:i" }}</div>
</div>
</td>
<td>
{% if log.automatic_report %}
<span class="badge badge-primary">
<i class="fas fa-clock"></i> {% trans "Automatisch" %}
</span>
{% else %}
<span class="badge badge-secondary">
<i class="fas fa-hand-paper"></i> {% trans "Manuell" %}
</span>
{% endif %}
</td>
<td>
{% if log.automatic_report %}
{{ log.automatic_report.name }}
{% else %}
{% trans "Manueller Report" %}
{% endif %}
</td>
<td>
<div class="date-range">
<div>{{ log.date_from|date:"d.m.Y" }}</div>
<div class="text-muted">{% trans "bis" %}</div>
<div>{{ log.date_to|date:"d.m.Y" }}</div>
</div>
</td>
<td>
<span class="patient-count">{{ log.patient_count }}</span>
</td>
<td>
{% if log.email_sent_to %}
<div class="email-list">
{% for email in log.email_sent_to %}
<span class="email-badge">{{ email }}</span>
{% endfor %}
</div>
{% else %}
<span class="text-muted">{% trans "Download" %}</span>
{% endif %}
</td>
<td>
{% if log.email_sent_to %}
<span class="badge badge-success">
<i class="fas fa-envelope"></i> {% trans "Gesendet" %}
</span>
{% else %}
<span class="badge badge-info">
<i class="fas fa-download"></i> {% trans "Heruntergeladen" %}
</span>
{% endif %}
</td>
<td>
<div class="action-buttons">
{% if log.csv_file %}
<a href="{{ log.csv_file.url }}" class="btn btn-sm btn-outline-primary" title="{% trans 'CSV herunterladen' %}">
<i class="fas fa-download"></i>
</a>
{% endif %}
<button class="btn btn-sm btn-outline-info"
onclick="showReportDetails({{ log.id }})"
title="{% trans 'Details anzeigen' %}">
<i class="fas fa-eye"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination (falls notwendig) -->
{% if is_paginated %}
<nav aria-label="Report log pagination">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">
<i class="fas fa-chevron-left"></i>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link">{{ num }}</span>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}">
<i class="fas fa-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="empty-state">
<div class="text-center py-5">
<i class="fas fa-file-alt fa-3x text-muted mb-3"></i>
<h4 class="text-muted">{% trans "Noch keine Reports erstellt" %}</h4>
<p class="text-muted">{% trans "Erstellen Sie Ihren ersten Report über das Dashboard." %}</p>
<a href="{% url 'reports:dashboard' %}" class="btn btn-primary">
<i class="fas fa-plus"></i> {% trans "Report erstellen" %}
</a>
</div>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Report Details Modal -->
<div class="modal fade" id="reportDetailsModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{% trans "Report-Details" %}</h5>
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div id="reportDetailsContent">
<!-- Content wird via JavaScript geladen -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">
{% trans "Schließen" %}
</button>
</div>
</div>
</div>
</div>
<style>
.datetime-cell .date {
font-weight: 500;
}
.datetime-cell .time {
font-size: 0.875em;
}
.date-range {
text-align: center;
}
.patient-count {
font-weight: 600;
color: #007bff;
}
.email-list {
max-width: 200px;
}
.email-badge {
display: inline-block;
background-color: #e9ecef;
color: #495057;
padding: 2px 6px;
border-radius: 3px;
font-size: 0.8em;
margin: 1px;
}
.action-buttons {
display: flex;
gap: 5px;
}
.empty-state {
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
.badge {
font-size: 0.75em;
padding: 4px 8px;
}
.badge-primary {
background-color: #007bff;
}
.badge-secondary {
background-color: #6c757d;
}
.badge-success {
background-color: #28a745;
}
.badge-info {
background-color: #17a2b8;
}
</style>
<script>
function showReportDetails(logId) {
// Placeholder für AJAX-Aufruf zum Laden der Report-Details
// In einer vollständigen Implementierung würde hier ein AJAX-Request
// an eine entsprechende View gemacht werden
const modal = $('#reportDetailsModal');
const content = $('#reportDetailsContent');
content.html(`
<div class="text-center">
<i class="fas fa-spinner fa-spin fa-2x"></i>
<p class="mt-2">{% trans "Lade Details..." %}</p>
</div>
`);
modal.modal('show');
// Simulierter Inhalt (in echter Implementierung via AJAX)
setTimeout(() => {
content.html(`
<div class="alert alert-info">
<strong>{% trans "Hinweis:" %}</strong>
{% trans "Die Detail-Ansicht wird in einer zukünftigen Version implementiert." %}
</div>
`);
}, 1000);
}
</script>
{% endblock %}

View file

@ -0,0 +1,32 @@
{% load i18n %}{% autoescape off %}
Hallo,
anbei erhalten Sie den angeforderten Report der Wildvogelhilfe Jena.
Report-Details:
- Zeitraum: {{ date_from }} bis {{ date_to }}
- Anzahl Patienten: {{ patient_count }}
{% if filter_naturschutzbehörde and filter_jagdbehörde %}
- Filter: Naturschutzbehörde und Jagdbehörde
{% elif filter_naturschutzbehörde %}
- Filter: Nur Naturschutzbehörde
{% elif filter_jagdbehörde %}
- Filter: Nur Jagdbehörde
{% endif %}
{% if automatic_report %}
- Automatischer Report: {{ automatic_report.name }}
{% else %}
- Report-Typ: Manuell erstellt
{% endif %}
Der Report liegt als CSV-Datei im Anhang bei und kann in Excel oder anderen Tabellenkalkulationsprogrammen geöffnet werden.
Bei Fragen zum Report wenden Sie sich bitte an das Team der Wildvogelhilfe Jena.
Mit freundlichen Grüßen
Wildvogelhilfe Jena e.V.
---
Diese E-Mail wurde automatisch generiert.
Erstellt am: {{ created_at }}
{% endautoescape %}

View file

@ -0,0 +1 @@
{% load i18n %}{% autoescape off %}{% blocktrans with site_name=current_site.name %}Wildvogelhilfe Jena Report von {{ date_from }} bis {{ date_to }}{% endblocktrans %}{% endautoescape %}