Add MCP tools for Veranstaltung participant management
- veranstaltungen_anzeigen: list events with participant counts - veranstaltung_teilnehmer_anzeigen: list participants by event - veranstaltung_teilnehmer_anlegen: add single participant - veranstaltung_teilnehmer_importieren: bulk import via JSON array Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1 +1,3 @@
|
|||||||
# MCP Server für die Stiftungsverwaltung
|
# MCP Server für die Stiftungsverwaltung
|
||||||
|
# Portabler MCP Server – kompatibel mit Claude Desktop, Cursor, Windsurf und
|
||||||
|
# allen MCP-kompatiblen AI-Tools. Siehe README.md für Einrichtung.
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ if _app_dir not in sys.path:
|
|||||||
sys.path.insert(0, _app_dir)
|
sys.path.insert(0, _app_dir)
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
|
||||||
|
os.environ.setdefault("DJANGO_ALLOW_ASYNC_UNSAFE", "true")
|
||||||
|
|
||||||
import django # noqa: E402
|
import django # noqa: E402
|
||||||
|
|
||||||
@@ -94,6 +95,8 @@ from mcp_server.tools.lesen import ( # noqa: E402
|
|||||||
statistiken,
|
statistiken,
|
||||||
termine_anzeigen,
|
termine_anzeigen,
|
||||||
transaktionen_suchen,
|
transaktionen_suchen,
|
||||||
|
veranstaltung_teilnehmer_anzeigen,
|
||||||
|
veranstaltungen_anzeigen,
|
||||||
verwaltungskosten,
|
verwaltungskosten,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -108,6 +111,8 @@ mcp.tool()(transaktionen_suchen)
|
|||||||
mcp.tool()(dokument_suchen)
|
mcp.tool()(dokument_suchen)
|
||||||
mcp.tool()(dokument_details)
|
mcp.tool()(dokument_details)
|
||||||
mcp.tool()(termine_anzeigen)
|
mcp.tool()(termine_anzeigen)
|
||||||
|
mcp.tool()(veranstaltungen_anzeigen)
|
||||||
|
mcp.tool()(veranstaltung_teilnehmer_anzeigen)
|
||||||
mcp.tool()(globale_suche)
|
mcp.tool()(globale_suche)
|
||||||
mcp.tool()(dashboard)
|
mcp.tool()(dashboard)
|
||||||
mcp.tool()(statistiken)
|
mcp.tool()(statistiken)
|
||||||
@@ -128,6 +133,8 @@ if can_write(_current_role):
|
|||||||
paechter_anlegen,
|
paechter_anlegen,
|
||||||
termin_anlegen,
|
termin_anlegen,
|
||||||
unterstuetzung_anlegen,
|
unterstuetzung_anlegen,
|
||||||
|
veranstaltung_teilnehmer_anlegen,
|
||||||
|
veranstaltung_teilnehmer_importieren,
|
||||||
verpachtung_anlegen,
|
verpachtung_anlegen,
|
||||||
verwaltungskosten_erfassen,
|
verwaltungskosten_erfassen,
|
||||||
)
|
)
|
||||||
@@ -142,6 +149,8 @@ if can_write(_current_role):
|
|||||||
mcp.tool()(verwaltungskosten_erfassen)
|
mcp.tool()(verwaltungskosten_erfassen)
|
||||||
mcp.tool()(termin_anlegen)
|
mcp.tool()(termin_anlegen)
|
||||||
mcp.tool()(dokument_verknuepfen)
|
mcp.tool()(dokument_verknuepfen)
|
||||||
|
mcp.tool()(veranstaltung_teilnehmer_anlegen)
|
||||||
|
mcp.tool()(veranstaltung_teilnehmer_importieren)
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
# Server starten
|
# Server starten
|
||||||
|
|||||||
@@ -569,6 +569,100 @@ def termine_anzeigen(
|
|||||||
return format_result({"anzahl": len(results), "termine": results})
|
return format_result({"anzahl": len(results), "termine": results})
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Veranstaltungen
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def veranstaltungen_anzeigen(
|
||||||
|
status: str = "",
|
||||||
|
limit: int = 20,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Zeigt Veranstaltungen der Stiftung an.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
status: geplant/einladungen_versendet/abgeschlossen/abgesagt (optional, leer = alle)
|
||||||
|
limit: Maximale Anzahl (max. 50)
|
||||||
|
"""
|
||||||
|
from stiftung.models.veranstaltungen import Veranstaltung
|
||||||
|
|
||||||
|
role = _get_role()
|
||||||
|
limit = min(limit, 50)
|
||||||
|
|
||||||
|
qs = Veranstaltung.objects.all()
|
||||||
|
if status:
|
||||||
|
qs = qs.filter(status=status)
|
||||||
|
|
||||||
|
qs = qs.order_by("-datum")[:limit]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for v in qs:
|
||||||
|
results.append({
|
||||||
|
"id": str(v.id),
|
||||||
|
"titel": v.titel,
|
||||||
|
"datum": v.datum.isoformat(),
|
||||||
|
"uhrzeit": v.uhrzeit.isoformat() if v.uhrzeit else None,
|
||||||
|
"ort": v.ort,
|
||||||
|
"status": v.status,
|
||||||
|
"teilnehmer_gesamt": v.get_teilnehmer_count(),
|
||||||
|
"zugesagt": v.get_zugesagte_count(),
|
||||||
|
"abgesagt": v.get_abgesagte_count(),
|
||||||
|
})
|
||||||
|
|
||||||
|
log_mcp_read(role, "veranstaltung", "Veranstaltungsübersicht", f"{len(results)} Veranstaltungen")
|
||||||
|
return format_result({"anzahl": len(results), "veranstaltungen": results})
|
||||||
|
|
||||||
|
|
||||||
|
def veranstaltung_teilnehmer_anzeigen(
|
||||||
|
veranstaltung_id: str,
|
||||||
|
rsvp_status: str = "",
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Zeigt die Teilnehmer einer Veranstaltung an.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
veranstaltung_id: UUID der Veranstaltung (Pflichtfeld)
|
||||||
|
rsvp_status: eingeladen/zugesagt/abgesagt/keine_rueckmeldung (optional, leer = alle)
|
||||||
|
"""
|
||||||
|
from stiftung.models.veranstaltungen import Veranstaltung
|
||||||
|
|
||||||
|
role = _get_role()
|
||||||
|
|
||||||
|
try:
|
||||||
|
veranstaltung = Veranstaltung.objects.get(id=veranstaltung_id)
|
||||||
|
except Veranstaltung.DoesNotExist:
|
||||||
|
return format_result({"fehler": f"Veranstaltung {veranstaltung_id} nicht gefunden"})
|
||||||
|
|
||||||
|
qs = veranstaltung.teilnehmer.all()
|
||||||
|
if rsvp_status:
|
||||||
|
qs = qs.filter(rsvp_status=rsvp_status)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for t in qs:
|
||||||
|
results.append({
|
||||||
|
"id": str(t.id),
|
||||||
|
"anrede": t.anrede,
|
||||||
|
"vorname": t.vorname,
|
||||||
|
"nachname": t.nachname,
|
||||||
|
"strasse": t.strasse,
|
||||||
|
"plz": t.plz,
|
||||||
|
"ort": t.ort,
|
||||||
|
"email": t.email,
|
||||||
|
"rsvp_status": t.rsvp_status,
|
||||||
|
"destinataer_id": str(t.destinataer_id) if t.destinataer_id else None,
|
||||||
|
})
|
||||||
|
|
||||||
|
log_mcp_read(
|
||||||
|
role, "veranstaltung", str(veranstaltung.id),
|
||||||
|
f"{len(results)} Teilnehmer von '{veranstaltung.titel}'",
|
||||||
|
)
|
||||||
|
return format_result({
|
||||||
|
"veranstaltung": str(veranstaltung),
|
||||||
|
"anzahl": len(results),
|
||||||
|
"teilnehmer": results,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
# Globale Suche & Dashboard
|
# Globale Suche & Dashboard
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -575,3 +575,158 @@ def dokument_verknuepfen(
|
|||||||
dokument.save(update_fields=update_fields)
|
dokument.save(update_fields=update_fields)
|
||||||
log_mcp_update(role, "dokumentlink", str(dokument.id), dokument.titel, changes)
|
log_mcp_update(role, "dokumentlink", str(dokument.id), dokument.titel, changes)
|
||||||
return format_result({"erfolg": True, "id": str(dokument.id), "verknuepft_mit": list(changes.keys())})
|
return format_result({"erfolg": True, "id": str(dokument.id), "verknuepft_mit": list(changes.keys())})
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Veranstaltungen – Teilnehmer
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def veranstaltung_teilnehmer_anlegen(
|
||||||
|
veranstaltung_id: str,
|
||||||
|
vorname: str,
|
||||||
|
nachname: str,
|
||||||
|
anrede: str = "",
|
||||||
|
strasse: str = "",
|
||||||
|
plz: str = "",
|
||||||
|
ort: str = "",
|
||||||
|
email: str = "",
|
||||||
|
rsvp_status: str = "eingeladen",
|
||||||
|
bemerkungen: str = "",
|
||||||
|
destinataer_id: str = "",
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Fügt einen Teilnehmer zu einer Veranstaltung hinzu.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
veranstaltung_id: UUID der Veranstaltung (Pflichtfeld)
|
||||||
|
vorname: Vorname (Pflichtfeld)
|
||||||
|
nachname: Nachname (Pflichtfeld)
|
||||||
|
anrede: Herr/Frau (optional). Akzeptiert auch 'Herrn' → wird zu 'Herr' normalisiert.
|
||||||
|
strasse: Straße und Hausnummer (optional)
|
||||||
|
plz: Postleitzahl (optional)
|
||||||
|
ort: Ort (optional)
|
||||||
|
email: E-Mail-Adresse (optional)
|
||||||
|
rsvp_status: eingeladen/zugesagt/abgesagt/keine_rueckmeldung (Standard: eingeladen)
|
||||||
|
bemerkungen: Freitext-Bemerkungen (optional)
|
||||||
|
destinataer_id: UUID eines bestehenden Destinatärs zum Verknüpfen (optional)
|
||||||
|
"""
|
||||||
|
from stiftung.models import Destinataer
|
||||||
|
from stiftung.models.veranstaltungen import Veranstaltung, Veranstaltungsteilnehmer
|
||||||
|
|
||||||
|
role = _require_write_role()
|
||||||
|
|
||||||
|
try:
|
||||||
|
veranstaltung = Veranstaltung.objects.get(id=veranstaltung_id)
|
||||||
|
except Veranstaltung.DoesNotExist:
|
||||||
|
return format_result({"fehler": f"Veranstaltung {veranstaltung_id} nicht gefunden"})
|
||||||
|
|
||||||
|
# Normalize anrede: 'Herrn' → 'Herr'
|
||||||
|
anrede_norm = anrede.strip()
|
||||||
|
if anrede_norm.lower() == "herrn":
|
||||||
|
anrede_norm = "Herr"
|
||||||
|
|
||||||
|
kwargs = {
|
||||||
|
"veranstaltung": veranstaltung,
|
||||||
|
"vorname": vorname.strip(),
|
||||||
|
"nachname": nachname.strip(),
|
||||||
|
"anrede": anrede_norm,
|
||||||
|
"strasse": strasse.strip(),
|
||||||
|
"plz": plz.strip(),
|
||||||
|
"ort": ort.strip(),
|
||||||
|
"email": email.strip(),
|
||||||
|
"rsvp_status": rsvp_status,
|
||||||
|
"bemerkungen": bemerkungen,
|
||||||
|
}
|
||||||
|
|
||||||
|
if destinataer_id:
|
||||||
|
try:
|
||||||
|
kwargs["destinataer"] = Destinataer.objects.get(id=destinataer_id)
|
||||||
|
except Destinataer.DoesNotExist:
|
||||||
|
return format_result({"fehler": f"Destinatär {destinataer_id} nicht gefunden"})
|
||||||
|
|
||||||
|
teilnehmer = Veranstaltungsteilnehmer.objects.create(**kwargs)
|
||||||
|
name = f"{vorname} {nachname}"
|
||||||
|
log_mcp_create(role, "veranstaltung", str(teilnehmer.id), f"Teilnehmer: {name}")
|
||||||
|
return format_result({
|
||||||
|
"erfolg": True,
|
||||||
|
"id": str(teilnehmer.id),
|
||||||
|
"name": name,
|
||||||
|
"veranstaltung": str(veranstaltung),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def veranstaltung_teilnehmer_importieren(
|
||||||
|
veranstaltung_id: str,
|
||||||
|
teilnehmer_liste: str,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Importiert mehrere Teilnehmer auf einmal in eine Veranstaltung.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
veranstaltung_id: UUID der Veranstaltung (Pflichtfeld)
|
||||||
|
teilnehmer_liste: JSON-Array mit Teilnehmerdaten. Jedes Objekt kann enthalten:
|
||||||
|
vorname (Pflicht), nachname (Pflicht), anrede, strasse, plz, ort, email,
|
||||||
|
rsvp_status, bemerkungen.
|
||||||
|
Beispiel: [{"vorname": "Max", "nachname": "Muster", "anrede": "Herr",
|
||||||
|
"strasse": "Musterstr. 1", "plz": "12345", "ort": "Berlin"}]
|
||||||
|
"""
|
||||||
|
import json as _json
|
||||||
|
|
||||||
|
from stiftung.models.veranstaltungen import Veranstaltung, Veranstaltungsteilnehmer
|
||||||
|
|
||||||
|
role = _require_write_role()
|
||||||
|
|
||||||
|
try:
|
||||||
|
veranstaltung = Veranstaltung.objects.get(id=veranstaltung_id)
|
||||||
|
except Veranstaltung.DoesNotExist:
|
||||||
|
return format_result({"fehler": f"Veranstaltung {veranstaltung_id} nicht gefunden"})
|
||||||
|
|
||||||
|
try:
|
||||||
|
teilnehmer_data = _json.loads(teilnehmer_liste)
|
||||||
|
except _json.JSONDecodeError as e:
|
||||||
|
return format_result({"fehler": f"Ungültiges JSON: {e}"})
|
||||||
|
|
||||||
|
if not isinstance(teilnehmer_data, list):
|
||||||
|
return format_result({"fehler": "teilnehmer_liste muss ein JSON-Array sein"})
|
||||||
|
|
||||||
|
erstellt = []
|
||||||
|
fehler = []
|
||||||
|
|
||||||
|
for idx, entry in enumerate(teilnehmer_data):
|
||||||
|
vorname = (entry.get("vorname") or "").strip()
|
||||||
|
nachname = (entry.get("nachname") or "").strip()
|
||||||
|
|
||||||
|
if not vorname or not nachname:
|
||||||
|
fehler.append({"index": idx, "grund": "vorname und nachname sind Pflichtfelder"})
|
||||||
|
continue
|
||||||
|
|
||||||
|
anrede = (entry.get("anrede") or "").strip()
|
||||||
|
if anrede.lower() == "herrn":
|
||||||
|
anrede = "Herr"
|
||||||
|
|
||||||
|
teilnehmer = Veranstaltungsteilnehmer.objects.create(
|
||||||
|
veranstaltung=veranstaltung,
|
||||||
|
vorname=vorname,
|
||||||
|
nachname=nachname,
|
||||||
|
anrede=anrede,
|
||||||
|
strasse=(entry.get("strasse") or "").strip(),
|
||||||
|
plz=(entry.get("plz") or "").strip(),
|
||||||
|
ort=(entry.get("ort") or "").strip(),
|
||||||
|
email=(entry.get("email") or "").strip(),
|
||||||
|
rsvp_status=entry.get("rsvp_status", "eingeladen"),
|
||||||
|
bemerkungen=entry.get("bemerkungen", ""),
|
||||||
|
)
|
||||||
|
erstellt.append({"id": str(teilnehmer.id), "name": f"{vorname} {nachname}"})
|
||||||
|
|
||||||
|
log_mcp_create(
|
||||||
|
role, "veranstaltung", str(veranstaltung.id),
|
||||||
|
f"{len(erstellt)} Teilnehmer importiert",
|
||||||
|
)
|
||||||
|
return format_result({
|
||||||
|
"erfolg": True,
|
||||||
|
"veranstaltung": str(veranstaltung),
|
||||||
|
"erstellt": len(erstellt),
|
||||||
|
"fehler": len(fehler),
|
||||||
|
"teilnehmer": erstellt,
|
||||||
|
"fehler_details": fehler if fehler else None,
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user