Files
kbcrm/App_Server/root/crm/model/projekt_user.py
T
gg 9ed5e88e01 Menü Projekt-Berechtigungen, Übersichtssichten und AppAdmin-Erkennung
- Menü: Dropdown "Projekt-Berechtigungen" unter Optionen mit
  Unterpunkten "Projekt-Zugriffe" und "Nutzer-Zugriffe" (nur SysAdmin/AppAdmin)
- AppAdmin-Erkennung: Server liefert isAppAdmin-Flag im Login- und
  getusers-Response (applikationsspezifisch via has_permission)
- Login: CurrentUserIsAppAdmin in sessionStorage speichern/entfernen
- Projekt-Zugriffe: geschachtelter Treetable (Projekt → Nutzer) als
  reine Anzeige, Detail-Sicht aus Projekt-Maske mit Bearbeitung
- Nutzer-Zugriffe: neue Komponente user_projektliste.ts, geschachtelter
  Treetable (Nutzer → Projekte), reine Anzeige
- Server: neuer Endpunkt GET /xhr/projektuser/listall (nur AppAdmin/SysAdmin)
- Server: get_all_projekt_users() mit Batch-Query für Projekte und User
- Treetable-Widget: Platzhalter-Pfeil bei kinderlosen Nodes nur wenn
  Geschwister mit Kindern existieren (hasSiblingsWithChildren)
- Datei projekt_user.ts umbenannt in projekt_userliste.ts (konsistent
  mit user_projektliste.ts)

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-07 16:04:06 +02:00

210 lines
6.8 KiB
Python

"""
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 <vibe@mistral.ai>
: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