""" Geschäftslogik für die Projekt-User-Zuordnung (Sichtbarkeit von Projekten). Diese Funktionen kapseln den DB-Zugriff auf die Tabelle 'projekt_users' und die Auflösung der user_id gegen die Auth-Datenbank (username, email etc.). Da CRM- und Auth-Datenbank getrennte SQLite-Dateien sind, erfolgt die Nutzer-Auflösung durch eine separate Query, nicht durch einen SQL-Join. Aufgerufen werden die Funktionen aus crm/routes.py; die Autorisierungsprüfung erfolgt dort über den Decorator authorisation_required. :author: Mistral Vibe :date: 2026-09-05 :version: 1.0 """ from db import dbapp from crm.model import ProjektUser, Projekt from auth.model import User from auth.model.errors import ApplicationError, HtmlStatusCode session = dbapp.session def get_projekt_users(projekt_id: int): """ Liefert alle einem Projekt zugeordneten Nutzer als Liste von Dictionaries. Jedes Dictionary enthält die Felder der ProjektUser-Zuordnung (id, projekt_id, user_id) sowie die aus der Auth-Datenbank aufgelösten Nutzerdaten (userid, username, email). - projekt_id: int - Die ID des Projekts, dessen Zuordnungen geliefert werden sollen. """ zuordnungen = ProjektUser.query.filter( ProjektUser.projekt_id == projekt_id ).all() if not zuordnungen: return [] # Alle user_ids sammeln und in einer einzigen Query gegen die Auth-DB # auflösen (Batch-Query statt N+1). user_ids = [z.user_id for z in zuordnungen] users = User.query.filter(User.id.in_(user_ids)).all() user_map = {u.id: u for u in users} result = [] for z in zuordnungen: eintrag = z.to_dict() user = user_map.get(z.user_id) if user is not None: eintrag['userid'] = user.userid eintrag['username'] = user.username eintrag['email'] = user.email or "" else: # Nutzer wurde aus der Auth-DB gelöscht, die Zuordnung existiert # noch. Die Felder werden leer geliefert. eintrag['userid'] = "" eintrag['username'] = "" eintrag['email'] = "" result.append(eintrag) return result def add_projekt_user(projekt_id: int, user_id: int) -> ProjektUser: """ Ordnet einen Nutzer einem Projekt zu. Das UniqueConstraint verhindert Duplikate. Wirft ApplicationError (404), falls das Projekt nicht existiert, oder (400), falls die Zuordnung bereits besteht oder die Nutzer-ID ungültig ist. - projekt_id: int - Die ID des Projekts. - user_id: int - Die ID (nicht userid) des Nutzers aus der Auth-Datenbank. """ projekt = Projekt.query.get(projekt_id) if projekt is None: raise ApplicationError( "Das Projekt mit der ID {} wurde nicht gefunden.".format(projekt_id), htmlstatus=HtmlStatusCode.Not_Found, ) if user_id is None or int(user_id) <= 0: raise ApplicationError( "Es muss ein gültiger Nutzer ausgewählt werden.", htmlstatus=HtmlStatusCode.Bad_Request, ) # Prüfen, ob der Nutzer in der Auth-Datenbank existiert. user = User.query.get(int(user_id)) if user is None: raise ApplicationError( "Der ausgewählte Nutzer existiert nicht.", htmlstatus=HtmlStatusCode.Bad_Request, ) # Duplikat-Prüfung (zusätzlich zum UniqueConstraint, um eine # aussagekräftige Fehlermeldung zu liefern). existing = ProjektUser.query.filter( ProjektUser.projekt_id == projekt_id, ProjektUser.user_id == int(user_id), ).first() if existing is not None: raise ApplicationError( "Der Nutzer ist diesem Projekt bereits zugeordnet.", htmlstatus=HtmlStatusCode.Bad_Request, ) zuordnung = ProjektUser() zuordnung.projekt_id = projekt_id zuordnung.user_id = int(user_id) session.add(zuordnung) session.commit() return zuordnung def remove_projekt_user(projekt_id: int, user_id: int): """ Entfernt die Zuordnung eines Nutzers zu einem Projekt. Wirft ApplicationError (404), falls die Zuordnung nicht existiert. - projekt_id: int - Die ID des Projekts. - user_id: int - Die ID (nicht userid) des Nutzers aus der Auth-Datenbank. """ zuordnung = ProjektUser.query.filter( ProjektUser.projekt_id == projekt_id, ProjektUser.user_id == int(user_id), ).first() if zuordnung is None: raise ApplicationError( "Die Zuordnung existiert nicht.", htmlstatus=HtmlStatusCode.Not_Found, ) session.delete(zuordnung) session.commit() def get_projekt_ids_for_user(user_id: int): """ Liefert alle Projekt-IDs, denen der Nutzer zugeordnet ist. Wird für die serverseitige Sichtbarkeits-Filterung in get_projekte und get_projekt verwendet. - user_id: int - Die ID (nicht userid) des Nutzers aus der Auth-Datenbank. """ zuordnungen = ProjektUser.query.filter( ProjektUser.user_id == user_id ).all() return [z.projekt_id for z in zuordnungen] def is_user_assigned(projekt_id: int, user_id: int) -> bool: """ Prüft, ob ein Nutzer einem Projekt zugeordnet ist. - projekt_id: int - Die ID des Projekts. - user_id: int - Die ID (nicht userid) des Nutzers aus der Auth-Datenbank. """ zuordnung = ProjektUser.query.filter( ProjektUser.projekt_id == projekt_id, ProjektUser.user_id == user_id, ).first() return zuordnung is not None def get_all_projekt_users(): """ Liefert alle Projekt-User-Zuordnungen, angereichert mit projektname (aus Projekt-Relationship) sowie userid, username und email (aus Auth-DB via Batch-Query). Wird für die Übersichtssicht "Nutzer-Zugriffe" verwendet, in der alle Zuordnungen in einem Aufruf geladen werden. """ zuordnungen = ProjektUser.query.all() if not zuordnungen: return [] # Projekte und Nutzer in jeweils einer Batch-Query auflösen. projekt_ids = list({z.projekt_id for z in zuordnungen}) user_ids = list({z.user_id for z in zuordnungen}) projekte = Projekt.query.filter(Projekt.id.in_(projekt_ids)).all() projekt_map = {p.id: p for p in projekte} users = User.query.filter(User.id.in_(user_ids)).all() user_map = {u.id: u for u in users} result = [] for z in zuordnungen: eintrag = z.to_dict() projekt = projekt_map.get(z.projekt_id) eintrag['projektname'] = projekt.projektname if projekt else "" user = user_map.get(z.user_id) if user is not None: eintrag['userid'] = user.userid eintrag['username'] = user.username eintrag['email'] = user.email or "" else: eintrag['userid'] = "" eintrag['username'] = "" eintrag['email'] = "" result.append(eintrag) return result