Files
kbcrm/App_Server/root/crm/model/projekt.py
T
gg 4cb0084066 Projekt-Sichtbarkeit: Nutzer-Zuordnung, projektbezogene Filterung und Schreibschutz
- Neues Modell ProjektUser (projekt_users) für projektbezogene Sichtbarkeit
- Migration V01_09 legt Tabelle an (keine Initial-Befüllung)
- CRUD-Logik für Projekt-User-Zuordnungen mit Cross-DB-User-Auflösung
- Serverseitige Sichtbarkeits-Filterung: Projekt-Liste/Get filtert nach
  projekt_users-Zuordnung für Nicht-SysAdmin/Nicht-AppAdmin
- Auto-Assignment: Ersteller wird bei Projektanlage automatisch zugeordnet
- Sichtbarkeits-Filterung auf CallLog und Kontaktadresse erweitert
  (Liste und Einzelabruf)
- Schreibschutz: Alle Schreib-Endpunkte (update/delete) für Projekt,
  Kontaktadresse und CallLog prüfen Projekt-Zuordnung vor der Ausführung
- Hilfsfunktionen _get_visible_user_id() und _require_projekt_access()
  zur Kapselung der wiederkehrenden Prüfungen
- REST-Endpunkte /xhr/projektuser/{list,add,remove};
  Schreib-Endpunkte mit @authorisation_required([]) als Erweiterungspunkt
- Client: Neues Modell projekt_user.ts, Listen-Komponente mit Modal
  (Select-Widget), Button in Projekt-Maske, Route registriert
- Build aktualisiert (tsc + rollup)
- AGENTS.md: Vorgaben für projektbezogene Sichtbarkeit dokumentiert

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-05 15:45:32 +02:00

168 lines
6.1 KiB
Python

"""
Geschäftslogik für die Entität Projekt.
Diese Funktionen kapseln den DB-Zugriff und die fachlichen Validierungen.
Aufgerufen werden sie aus crm/routes.py; die Autorisierungsprüfung erfolgt
dort über den Decorator authorisation_required.
:author: Mistral Vibe <vibe@mistral.ai>
:date: 2026-09-04
:version: 1.0
"""
from db import dbapp
from crm.model import Projekt, Kunde, ProjektUser
from auth.model.errors import ApplicationError, HtmlStatusCode
from flask_login import current_user
current_user: object
session = dbapp.session
def get_projekte(kunden_id: int | None = None, user_id: int | None = None):
"""
Liefert Projekte, sortiert nach projektname.
Ist kunden_id gesetzt, werden nur die Projekte dieses Kunden
zurückgegeben (serverseitige Filterung). Ohne kunden_id werden
alle Projekte geliefert.
Ist user_id gesetzt, werden nur Projekte geliefert, denen der
Nutzer über die Tabelle 'projekt_users' zugeordnet ist. Dies
steuert die Sichtbarkeit: Nicht zugeordnete Nutzer sehen diese
Projekte nicht. SysAdmin und AppAdmin übergeben user_id=None
und sehen alle Projekte.
"""
query = Projekt.query
if kunden_id is not None:
query = query.filter(Projekt.kunden_id == kunden_id)
if user_id is not None:
# Nur Projekte, denen der Nutzer zugeordnet ist. Der Subquery
# auf projekt_users filtert die sichtbaren Projekt-IDs.
query = query.filter(
Projekt.id.in_(
dbapp.session.query(ProjektUser.projekt_id).filter(
ProjektUser.user_id == user_id
)
)
)
return query.order_by(Projekt.projektname).all()
def get_projekt(projekt_id: int, user_id: int | None = None) -> Projekt:
"""
Liefert ein einzelnes Projekt anhand seiner ID.
Wirft ApplicationError (404), falls das Projekt nicht existiert.
Ist user_id gesetzt, wird zusätzlich geprüft, ob der Nutzer dem
Projekt zugeordnet ist. Ist er nicht zugeordnet, wird ein 404
geliefert, damit nicht zugeordnete Nutzer keine Rückschlüsse auf
die Existenz des Projekts ziehen können.
"""
projekt = Projekt.query.get(projekt_id)
if projekt is None:
raise ApplicationError(
"Projekt mit der ID {} wurde nicht gefunden.".format(projekt_id),
htmlstatus=HtmlStatusCode.Not_Found,
)
if user_id is not None:
# Sichtbarkeitsprüfung: Ist der Nutzer dem Projekt zugeordnet?
zuordnung = ProjektUser.query.filter(
ProjektUser.projekt_id == projekt_id,
ProjektUser.user_id == user_id,
).first()
if zuordnung is None:
# 404 statt 403, damit nicht zugeordnete Nutzer keine Info über
# die Existenz des Projekts erhalten.
raise ApplicationError(
"Projekt mit der ID {} wurde nicht gefunden.".format(projekt_id),
htmlstatus=HtmlStatusCode.Not_Found,
)
return projekt
def create_projekt(data: dict) -> Projekt:
"""
Legt ein neues Projekt an. Die vom Client übergebene ID wird ignoriert;
die ID wird serverseitig per Auto-Increment vergeben.
- data: dict mit den Feldern kunden_id, projektname, zweck,
verantwortlicher, startdatum
"""
kunden_id = data.get("kunden_id")
if kunden_id is None:
raise ApplicationError(
"Es muss ein Kunde ausgewählt werden.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
kunde = Kunde.query.get(int(kunden_id))
if kunde is None:
raise ApplicationError(
"Der ausgewählte Kunde existiert nicht.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
if not data.get("projektname") or not str(data["projektname"]).strip():
raise ApplicationError(
"Der Projektname darf nicht leer sein.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
projekt = Projekt()
projekt.kunden_id = int(kunden_id)
projekt.projektname = data["projektname"].strip()
projekt.zweck = (data.get("zweck") or "").strip() or None
projekt.verantwortlicher = (data.get("verantwortlicher") or "").strip() or None
projekt.startdatum = (data.get("startdatum") or "").strip() or None
session.add(projekt)
session.commit()
# Auto-Assignment: Den Ersteller dem neuen Projekt zuordnen, damit
# er es sehen kann. Nur für authentifizierte Nutzer mit einer id.
if hasattr(current_user, "id") and current_user.id is not None:
zuordnung = ProjektUser()
zuordnung.projekt_id = projekt.id
zuordnung.user_id = current_user.id
session.add(zuordnung)
session.commit()
return projekt
def update_projekt(data: dict) -> Projekt:
"""
Aktualisiert ein bestehendes Projekt. Die ID im data-Dict bestimmt den
zu ändernden Datensatz.
"""
projekt = get_projekt(data["id"])
kunden_id = data.get("kunden_id")
if kunden_id is None:
raise ApplicationError(
"Es muss ein Kunde ausgewählt werden.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
kunde = Kunde.query.get(int(kunden_id))
if kunde is None:
raise ApplicationError(
"Der ausgewählte Kunde existiert nicht.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
if not data.get("projektname") or not str(data["projektname"]).strip():
raise ApplicationError(
"Der Projektname darf nicht leer sein.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
projekt.kunden_id = int(kunden_id)
projekt.projektname = data["projektname"].strip()
projekt.zweck = (data.get("zweck") or "").strip() or None
projekt.verantwortlicher = (data.get("verantwortlicher") or "").strip() or None
projekt.startdatum = (data.get("startdatum") or "").strip() or None
session.commit()
return projekt
def delete_projekt(projekt_id: int):
"""
Löscht ein Projekt anhand seiner ID.
Wirft ApplicationError (404), falls das Projekt nicht existiert.
"""
projekt = get_projekt(projekt_id)
session.delete(projekt)
session.commit()