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

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)