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>
This commit is contained in:
1 parent
b2f8684afb
commit
4cb0084066
12 files changed
+758
-25
No files matched your search
@@ -0,0 +1,167 @@
|
||||
"""
|
||||
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
|
||||
Reference in new issue
Block a user