Add notification settings and forms for email notifications

This commit is contained in:
Maximilian 2025-06-10 09:08:08 +02:00
parent 93f5f05a33
commit bb8949af76
19 changed files with 435 additions and 23 deletions

View file

@ -21,7 +21,14 @@ class FallenBirdAdmin(admin.ModelAdmin):
@admin.register(Bird)
class BirdAdmin(admin.ModelAdmin):
list_display = ["name"]
list_display = ["name", "melden_an_naturschutzbehoerde", "melden_an_jagdbehoerde", "melden_an_wildvogelhilfe_team"]
list_filter = ["melden_an_naturschutzbehoerde", "melden_an_jagdbehoerde", "melden_an_wildvogelhilfe_team"]
fields = ('name', 'description', 'melden_an_naturschutzbehoerde', 'melden_an_jagdbehoerde', 'melden_an_wildvogelhilfe_team')
def save_model(self, request, obj, form, change):
if not change: # Only set created_by when creating new object
obj.created_by = request.user
super().save_model(request, obj, form, change)
@admin.register(BirdStatus)

View file

@ -75,3 +75,30 @@ class BirdEditForm(forms.ModelForm):
"finder": _("Finder"),
"comment": _("Bermerkung"),
}
class BirdSpeciesForm(forms.ModelForm):
"""Form for editing Bird species with notification settings."""
class Meta:
model = Bird
fields = [
"name",
"description",
"species",
"melden_an_naturschutzbehoerde",
"melden_an_jagdbehoerde",
"melden_an_wildvogelhilfe_team",
]
labels = {
"name": _("Bezeichnung"),
"description": _("Erläuterungen"),
"species": _("Art"),
"melden_an_naturschutzbehoerde": _("Melden an Naturschutzbehörde"),
"melden_an_jagdbehoerde": _("Melden an Jagdbehörde"),
"melden_an_wildvogelhilfe_team": _("Melden an Wildvogelhilfe-Team"),
}
help_texts = {
"melden_an_naturschutzbehoerde": _("Automatische E-Mail-Benachrichtigung an Naturschutzbehörde senden"),
"melden_an_jagdbehoerde": _("Automatische E-Mail-Benachrichtigung an Jagdbehörde senden"),
"melden_an_wildvogelhilfe_team": _("Automatische E-Mail-Benachrichtigung an Wildvogelhilfe-Team senden"),
}

View file

@ -0,0 +1,28 @@
# Generated manually for notification settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bird', '0006_alter_fallenbird_options_alter_fallenbird_age_and_more'),
]
operations = [
migrations.AddField(
model_name='bird',
name='melden_an_naturschutzbehoerde',
field=models.BooleanField(default=True, verbose_name='Melden an Naturschutzbehörde'),
),
migrations.AddField(
model_name='bird',
name='melden_an_jagdbehoerde',
field=models.BooleanField(default=False, verbose_name='Melden an Jagdbehörde'),
),
migrations.AddField(
model_name='bird',
name='melden_an_wildvogelhilfe_team',
field=models.BooleanField(default=True, verbose_name='Melden an Wildvogelhilfe-Team'),
),
]

View file

@ -0,0 +1,41 @@
# Data migration to set defaults for existing Bird records
from django.db import migrations
def set_default_notification_settings(apps, schema_editor):
"""Set default notification settings for all existing Bird records."""
Bird = apps.get_model('bird', 'Bird')
# Update all existing birds to have the default notification settings
Bird.objects.all().update(
melden_an_naturschutzbehoerde=True,
melden_an_wildvogelhilfe_team=True,
melden_an_jagdbehoerde=False
)
def reverse_default_notification_settings(apps, schema_editor):
"""Reverse the default settings if needed."""
Bird = apps.get_model('bird', 'Bird')
# Reset all notification settings to False
Bird.objects.all().update(
melden_an_naturschutzbehoerde=False,
melden_an_wildvogelhilfe_team=False,
melden_an_jagdbehoerde=False
)
class Migration(migrations.Migration):
dependencies = [
('bird', '0007_add_notification_settings'),
]
operations = [
migrations.RunPython(
set_default_notification_settings,
reverse_default_notification_settings
),
]

View file

@ -0,0 +1,14 @@
# Generated by Django 5.2.2 on 2025-06-09 18:33
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('bird', '0007_alter_fallenbird_status'),
('bird', '0008_set_default_notification_settings'),
]
operations = [
]

View file

@ -193,6 +193,20 @@ class Bird(models.Model):
updated = models.DateTimeField(
auto_now=True, verbose_name=_("Geändert am")
)
# New notification settings fields - "Melden an" section
melden_an_naturschutzbehoerde = models.BooleanField(
default=True,
verbose_name=_("Melden an Naturschutzbehörde")
)
melden_an_jagdbehoerde = models.BooleanField(
default=False,
verbose_name=_("Melden an Jagdbehörde")
)
melden_an_wildvogelhilfe_team = models.BooleanField(
default=True,
verbose_name=_("Melden an Wildvogelhilfe-Team")
)
class Meta:
verbose_name = _("Vogel")

View file

@ -0,0 +1,50 @@
{% extends "base.html" %}
{% load static %}
{% load crispy_forms_tags %}
{% block content %}
<h3>E-Mail-Benachrichtigungen für {{ bird_species.name }} bearbeiten</h3>
<div class="row">
<div class="col-lg-6 mb-3">
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<a href="{% url 'bird_species_list' %}" class="btn btn-success">Abbrechen</a>
<button class="btn btn-primary" type="submit">Speichern</button>
</form>
</div>
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<h5>Informationen zu E-Mail-Benachrichtigungen</h5>
</div>
<div class="card-body">
<h6>Naturschutzbehörde</h6>
<p class="small">
Wenn aktiviert, wird automatisch eine E-Mail an alle als "Naturschutzbehörde"
markierten E-Mail-Adressen gesendet, wenn ein Vogel dieser Art gefunden wird.
</p>
<h6>Jagdbehörde</h6>
<p class="small">
Wenn aktiviert, wird automatisch eine E-Mail an alle als "Jagdbehörde"
markierten E-Mail-Adressen gesendet, wenn ein Vogel dieser Art gefunden wird.
</p>
<h6>Wildvogelhilfe-Team</h6>
<p class="small">
Wenn aktiviert, wird automatisch eine E-Mail an alle als "Wildvogelhilfe-Team"
markierten E-Mail-Adressen gesendet, wenn ein Vogel dieser Art gefunden wird.
</p>
<div class="alert alert-info mt-3">
<strong>Hinweis:</strong> Für neue Vogelarten werden standardmäßig
"Naturschutzbehörde" und "Wildvogelhilfe-Team" aktiviert.
</div>
</div>
</div>
</div>
</div>
{% endblock content %}

View file

@ -0,0 +1,58 @@
{% extends "base.html" %}
{% load static %}
{% block content %}
<h3>Vogelarten - E-Mail-Benachrichtigungen verwalten</h3>
<div class="row">
<div class="col-lg-12 mb-3">
<p>
Hier können Sie für jede Vogelart konfigurieren, welche Behörden und Teams
automatisch benachrichtigt werden sollen, wenn ein Vogel dieser Art gefunden wird.
</p>
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Vogelart</th>
<th>Naturschutzbehörde</th>
<th>Jagdbehörde</th>
<th>Wildvogelhilfe-Team</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for bird in birds %}
<tr>
<td><strong>{{ bird.name }}</strong></td>
<td>
{% if bird.melden_an_naturschutzbehoerde %}
<span class="badge bg-success">Aktiv</span>
{% else %}
<span class="badge bg-secondary">Inaktiv</span>
{% endif %}
</td>
<td>
{% if bird.melden_an_jagdbehoerde %}
<span class="badge bg-success">Aktiv</span>
{% else %}
<span class="badge bg-secondary">Inaktiv</span>
{% endif %}
</td>
<td>
{% if bird.melden_an_wildvogelhilfe_team %}
<span class="badge bg-success">Aktiv</span>
{% else %}
<span class="badge bg-secondary">Inaktiv</span>
{% endif %}
</td>
<td>
<a href="{% url 'bird_species_edit' bird.id %}" class="btn btn-sm btn-primary">Bearbeiten</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock content %}

View file

@ -8,6 +8,8 @@ from .views import (
bird_help_single,
bird_inactive,
bird_single,
bird_species_list,
bird_species_edit,
)
urlpatterns = [
@ -17,5 +19,7 @@ urlpatterns = [
path("delete/<id>", bird_delete, name="bird_delete"),
path("help/", bird_help, name="bird_help"),
path("help/<id>", bird_help_single, name="bird_help_single"),
path("species/", bird_species_list, name="bird_species_list"),
path("species/<id>/edit/", bird_species_edit, name="bird_species_edit"),
path("<id>/", bird_single, name="bird_single"),
]

View file

@ -7,11 +7,11 @@ from django.shortcuts import redirect, render, HttpResponse
from django.core.mail import send_mail, BadHeaderError
from smtplib import SMTPException
from .forms import BirdAddForm, BirdEditForm
from .forms import BirdAddForm, BirdEditForm, BirdSpeciesForm
from .models import Bird, FallenBird
from sendemail.message import messagebody
from sendemail.models import BirdEmail
from sendemail.models import BirdEmail, Emailadress
env = environ.Env()
@ -33,24 +33,42 @@ def bird_create(request):
fs.save()
request.session["rescuer_id"] = None
# Send email to all related email addresses
email_addresses = BirdEmail.objects.filter(bird=fs.bird_id)
# Send email to all related email addresses based on bird species notification settings
bird = Bird.objects.get(id=fs.bird_id)
try:
send_mail(
subject="Wildvogel gefunden!",
message=messagebody(
fs.date_found, bird, fs.place, fs.diagnostic_finding
),
from_email=env("DEFAULT_FROM_EMAIL"),
recipient_list=[
email.email.email_address for email in email_addresses
],
)
except BadHeaderError:
return HttpResponse("Invalid header found.")
except SMTPException as e:
print("There was an error sending an email: ", e)
# Get email addresses that match the bird species' notification settings
email_addresses = []
# Check each notification category and add matching email addresses
if bird.melden_an_naturschutzbehoerde:
naturschutz_emails = Emailadress.objects.filter(is_naturschutzbehoerde=True)
email_addresses.extend([email.email_address for email in naturschutz_emails])
if bird.melden_an_jagdbehoerde:
jagd_emails = Emailadress.objects.filter(is_jagdbehoerde=True)
email_addresses.extend([email.email_address for email in jagd_emails])
if bird.melden_an_wildvogelhilfe_team:
team_emails = Emailadress.objects.filter(is_wildvogelhilfe_team=True)
email_addresses.extend([email.email_address for email in team_emails])
# Remove duplicates
email_addresses = list(set(email_addresses))
if email_addresses: # Only send if there are recipients
try:
send_mail(
subject="Wildvogel gefunden!",
message=messagebody(
fs.date_found, bird, fs.place, fs.diagnostic_finding
),
from_email=env("DEFAULT_FROM_EMAIL"),
recipient_list=email_addresses,
)
except BadHeaderError:
return HttpResponse("Invalid header found.")
except SMTPException as e:
print("There was an error sending an email: ", e)
return redirect("bird_all")
context = {"form": form}
@ -119,3 +137,26 @@ def bird_delete(request, id):
return redirect("bird_all")
context = {"bird": bird}
return render(request, "bird/bird_delete.html", context)
@login_required(login_url="account_login")
def bird_species_list(request):
"""List all bird species with their notification settings."""
birds = Bird.objects.all().order_by("name")
context = {"birds": birds}
return render(request, "bird/bird_species_list.html", context)
@login_required(login_url="account_login")
def bird_species_edit(request, id):
"""Edit bird species notification settings."""
bird_species = Bird.objects.get(id=id)
form = BirdSpeciesForm(request.POST or None, instance=bird_species)
if request.method == "POST":
if form.is_valid():
form.save()
return redirect("bird_species_list")
context = {"form": form, "bird_species": bird_species}
return render(request, "bird/bird_species_edit.html", context)

View file

@ -233,6 +233,12 @@ STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
STATIC_ROOT = BASE_DIR / "staticfiles"
# -----------------------------------
# Media files (User uploaded content)
# -----------------------------------
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
# -----------------------------------
# Email
# -----------------------------------

View file

@ -1,5 +1,7 @@
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from bird import views
urlpatterns = [
@ -14,6 +16,12 @@ urlpatterns = [
path("admin/", admin.site.urls),
# Allauth
path("accounts/", include("allauth.urls")),
# CKEditor 5
path("ckeditor5/", include('django_ckeditor_5.urls')),
# Static sites
# path("", include("sites.urls")),
]
# Serve media files during development
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View file

@ -5,10 +5,24 @@ from .models import Emailadress, BirdEmail
@admin.register(Emailadress)
class EmailaddressAdmin(admin.ModelAdmin):
list_display = ["email_address", "created_at", "updated_at", "user"]
list_display = ["email_address", "is_naturschutzbehoerde", "is_jagdbehoerde", "is_wildvogelhilfe_team", "created_at", "updated_at", "user"]
search_fields = ["email_address"]
list_filter = ["created_at", "updated_at", "user"]
list_filter = ["is_naturschutzbehoerde", "is_jagdbehoerde", "is_wildvogelhilfe_team", "created_at", "updated_at", "user"]
list_per_page = 20
fieldsets = (
(None, {
'fields': ('email_address',)
}),
('Notification Categories', {
'fields': ('is_naturschutzbehoerde', 'is_jagdbehoerde', 'is_wildvogelhilfe_team'),
'description': 'Select which types of notifications this email address should receive'
}),
)
def save_model(self, request, obj, form, change):
if not change: # Only set user when creating new object
obj.user = request.user
super().save_model(request, obj, form, change)
@admin.register(BirdEmail)

30
app/sendemail/forms.py Normal file
View file

@ -0,0 +1,30 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from .models import Emailadress
class EmailaddressForm(forms.ModelForm):
"""Form for editing email addresses with notification categories."""
class Meta:
model = Emailadress
fields = [
"email_address",
"is_naturschutzbehoerde",
"is_jagdbehoerde",
"is_wildvogelhilfe_team",
]
labels = {
"email_address": _("E-Mail-Adresse"),
"is_naturschutzbehoerde": _("Naturschutzbehörde"),
"is_jagdbehoerde": _("Jagdbehörde"),
"is_wildvogelhilfe_team": _("Wildvogelhilfe-Team"),
}
help_texts = {
"is_naturschutzbehoerde": _("Diese Adresse für Naturschutzbehörden-Benachrichtigungen verwenden"),
"is_jagdbehoerde": _("Diese Adresse für Jagdbehörden-Benachrichtigungen verwenden"),
"is_wildvogelhilfe_team": _("Diese Adresse für Wildvogelhilfe-Team-Benachrichtigungen verwenden"),
}
widgets = {
"email_address": forms.EmailInput(attrs={"class": "form-control"}),
}

View file

@ -0,0 +1,28 @@
# Generated manually for notification categories
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sendemail', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='emailadress',
name='is_naturschutzbehoerde',
field=models.BooleanField(default=False, verbose_name='Naturschutzbehörde'),
),
migrations.AddField(
model_name='emailadress',
name='is_jagdbehoerde',
field=models.BooleanField(default=False, verbose_name='Jagdbehörde'),
),
migrations.AddField(
model_name='emailadress',
name='is_wildvogelhilfe_team',
field=models.BooleanField(default=False, verbose_name='Wildvogelhilfe-Team'),
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 5.2.2 on 2025-06-10 06:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sendemail', '0002_add_notification_categories'),
]
operations = [
migrations.AlterField(
model_name='emailadress',
name='is_naturschutzbehoerde',
field=models.BooleanField(default=True, verbose_name='Naturschutzbehörde'),
),
migrations.AlterField(
model_name='emailadress',
name='is_wildvogelhilfe_team',
field=models.BooleanField(default=True, verbose_name='Wildvogelhilfe-Team'),
),
]

View file

@ -14,6 +14,20 @@ class Emailadress(models.Model):
on_delete=models.CASCADE,
verbose_name=_("Benutzer"),
)
# New notification category fields
is_naturschutzbehoerde = models.BooleanField(
default=True,
verbose_name=_("Naturschutzbehörde")
)
is_jagdbehoerde = models.BooleanField(
default=False,
verbose_name=_("Jagdbehörde")
)
is_wildvogelhilfe_team = models.BooleanField(
default=True,
verbose_name=_("Wildvogelhilfe-Team")
)
def __str__(self):
return self.email_address

View file

@ -36,6 +36,10 @@
<a class="nav-link {% if '/contacts' in request.path %} active {% endif %}"
href="{% url 'contact_all' %}">Kontakte</a>
</li>
<li class="nav-item">
<a class="nav-link {% if '/bird/species' in request.path %} active {% endif %}"
href="{% url 'bird_species_list' %}">Vogelarten</a>
</li>
{% if request.user|group_check:"data-export" %}
<li class="nav-item">

View file

@ -31,7 +31,8 @@ services:
- db
labels:
- "traefik.enable=true"
- "traefik.http.routers.django.rule=Host(`${ALLOWED_HOSTS}`)"
- "traefik.http.routers.web.rule=Host(`${ALLOWED_HOSTS}`)"
- "traefik.http.services.web.loadbalancer.server.port=8000"
db:
image: postgres:15-alpine