Merge branch 'report_mail'
This commit is contained in:
commit
acb398be1c
30 changed files with 2220 additions and 8 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -180,4 +180,8 @@ postgres
|
||||||
|
|
||||||
# Prestatic Collections
|
# Prestatic Collections
|
||||||
app/staticfiles
|
app/staticfiles
|
||||||
|
|
||||||
|
# Reports CSV files
|
||||||
|
app/media/reports/csv/*
|
||||||
|
|
||||||
TODO.md
|
TODO.md
|
||||||
|
|
22
README.md
22
README.md
|
@ -88,6 +88,28 @@ python3 -m pytest test/ --cov=app --cov-report=html
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 📊 Reports System
|
||||||
|
|
||||||
|
Das Django FBF verfügt über ein vollständiges Reports-System für die Wildvogelhilfe Jena:
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- **Manuelle Reports**: Interaktive Erstellung mit Datumsbereich und Filteroptionen
|
||||||
|
- **Automatische Reports**: Wiederkehrende Reports (wöchentlich/monatlich/quartalsweise)
|
||||||
|
- **E-Mail-Versand**: Professional formatierte E-Mails mit CSV-Anhang
|
||||||
|
- **Report-Protokoll**: Vollständige Audit-Spur aller generierten Reports
|
||||||
|
- **Admin-Integration**: Nahtlose Integration in Django Admin
|
||||||
|
|
||||||
|
### Verwendung
|
||||||
|
1. **Admin-Panel öffnen**: [http://localhost:8008/admin/reports/](http://localhost:8008/admin/reports/)
|
||||||
|
2. **Manuelle Reports**: "Report erstellen" → Datum wählen → Filter setzen → Herunterladen/E-Mail
|
||||||
|
3. **Automatische Reports**: Wiederkehrende Reports konfigurieren mit E-Mail-Verteilern
|
||||||
|
4. **Report-Logs**: Verlauf aller Reports mit Download-Möglichkeit
|
||||||
|
|
||||||
|
### CSV-Export
|
||||||
|
Reports enthalten folgende Felder: Vogel, Alter, Geschlecht, Gefunden am, Fundort, Fundumstände, Diagnose bei Fund, Status
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Throw old database
|
## Throw old database
|
||||||
In case you've got an preexisting database, delete it and do the following:
|
In case you've got an preexisting database, delete it and do the following:
|
||||||
|
|
||||||
|
|
|
@ -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)
|
# 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"],
|
# "order_with_respect_to": ["auth", "books", "books.author", "books.book"],
|
||||||
# Custom links to append to app groups, keyed on app name
|
# Custom links to append to app groups, keyed on app name
|
||||||
# "custom_links": {
|
"custom_links": {
|
||||||
# "books": [{
|
"reports": [{
|
||||||
# "name": "Make Messages",
|
"name": "Reports Dashboard",
|
||||||
# "url": "make_messages",
|
"url": "/admin/reports/",
|
||||||
# "icon": "fas fa-comments",
|
"icon": "fas fa-chart-bar",
|
||||||
# "permissions": ["books.view_book"]
|
"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
|
# 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
|
# for the full list of 5.13.0 free icon classes
|
||||||
"icons": {
|
"icons": {
|
||||||
|
@ -89,6 +89,9 @@ JAZZMIN_SETTINGS = {
|
||||||
"contact.Contact": "fas fa-solid fa-address-card",
|
"contact.Contact": "fas fa-solid fa-address-card",
|
||||||
"contact.ContactTag": "fas fa-solid fa-tags",
|
"contact.ContactTag": "fas fa-solid fa-tags",
|
||||||
"sendemail.Emailadress": "fas fa-solid fa-envelope",
|
"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
|
# Icons that are used when one is not manually specified
|
||||||
# "default_icon_parents": "fas fa-chevron-circle-right",
|
# "default_icon_parents": "fas fa-chevron-circle-right",
|
||||||
|
|
|
@ -85,6 +85,7 @@ INSTALLED_APPS = [
|
||||||
"contact",
|
"contact",
|
||||||
"costs",
|
"costs",
|
||||||
"export",
|
"export",
|
||||||
|
"reports",
|
||||||
"sendemail",
|
"sendemail",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ urlpatterns = [
|
||||||
path("export/", include("export.urls")),
|
path("export/", include("export.urls")),
|
||||||
# Admin
|
# Admin
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
|
path("admin/reports/", include("reports.urls", namespace="reports")),
|
||||||
# Allauth
|
# Allauth
|
||||||
path("accounts/", include("allauth.urls")),
|
path("accounts/", include("allauth.urls")),
|
||||||
# CKEditor 5
|
# CKEditor 5
|
||||||
|
|
0
app/reports/__init__.py
Normal file
0
app/reports/__init__.py
Normal file
123
app/reports/admin.py
Normal file
123
app/reports/admin.py
Normal 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
8
app/reports/apps.py
Normal 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
157
app/reports/forms.py
Normal 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
|
1
app/reports/management/__init__.py
Normal file
1
app/reports/management/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Empty file to make this directory a Python package
|
1
app/reports/management/commands/__init__.py
Normal file
1
app/reports/management/commands/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Empty file to make this directory a Python package
|
62
app/reports/management/commands/create_test_data.py
Normal file
62
app/reports/management/commands/create_test_data.py
Normal 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!')
|
||||||
|
)
|
129
app/reports/management/commands/test_reports.py
Normal file
129
app/reports/management/commands/test_reports.py
Normal 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}')
|
||||||
|
)
|
62
app/reports/migrations/0001_initial.py
Normal file
62
app/reports/migrations/0001_initial.py
Normal 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'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
0
app/reports/migrations/__init__.py
Normal file
0
app/reports/migrations/__init__.py
Normal file
146
app/reports/models.py
Normal file
146
app/reports/models.py
Normal 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
187
app/reports/services.py
Normal 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
3
app/reports/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
14
app/reports/urls.py
Normal file
14
app/reports/urls.py
Normal 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
178
app/reports/views.py
Normal 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)
|
104
app/templates/admin/reports/automatic_report_confirm_delete.html
Normal file
104
app/templates/admin/reports/automatic_report_confirm_delete.html
Normal 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 %}
|
145
app/templates/admin/reports/automatic_report_form.html
Normal file
145
app/templates/admin/reports/automatic_report_form.html
Normal 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 %}
|
157
app/templates/admin/reports/automatic_reports.html
Normal file
157
app/templates/admin/reports/automatic_reports.html
Normal 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 %}
|
72
app/templates/admin/reports/base.html
Normal file
72
app/templates/admin/reports/base.html
Normal 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 %}
|
85
app/templates/admin/reports/dashboard.html
Normal file
85
app/templates/admin/reports/dashboard.html
Normal 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 %}
|
178
app/templates/admin/reports/manual_report.html
Normal file
178
app/templates/admin/reports/manual_report.html
Normal 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 %}
|
278
app/templates/admin/reports/report_logs.html
Normal file
278
app/templates/admin/reports/report_logs.html
Normal 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">×</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 %}
|
32
app/templates/reports/email/report_message.txt
Normal file
32
app/templates/reports/email/report_message.txt
Normal 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 %}
|
1
app/templates/reports/email/report_subject.txt
Normal file
1
app/templates/reports/email/report_subject.txt
Normal 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 %}
|
Loading…
Add table
Add a link
Reference in a new issue