""" 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 :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()