fix: resolve STT AttributeError and int/str TypeError in teamsbot service
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
7778325e5e
commit
2d7da8a66d
5 changed files with 54 additions and 286 deletions
|
|
@ -490,7 +490,7 @@ def _getEffectiveConfig(instanceId: str, userId: str, interface) -> TeamsbotConf
|
||||||
overrides[field] = value
|
overrides[field] = value
|
||||||
|
|
||||||
if overrides:
|
if overrides:
|
||||||
return baseConfig.model_copy(update=overrides)
|
return TeamsbotConfig.model_validate({**baseConfig.model_dump(), **overrides})
|
||||||
return baseConfig
|
return baseConfig
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1228,7 +1228,7 @@ async def botWebsocket(
|
||||||
if value is not None:
|
if value is not None:
|
||||||
overrides[field] = value
|
overrides[field] = value
|
||||||
if overrides:
|
if overrides:
|
||||||
config = config.model_copy(update=overrides)
|
config = TeamsbotConfig.model_validate({**config.model_dump(), **overrides})
|
||||||
logger.info(f"Browser Bot WebSocket: Applied user settings overrides: {list(overrides.keys())}")
|
logger.info(f"Browser Bot WebSocket: Applied user settings overrides: {list(overrides.keys())}")
|
||||||
|
|
||||||
service = TeamsbotService(originalUser, mandateId, instanceId, config)
|
service = TeamsbotService(originalUser, mandateId, instanceId, config)
|
||||||
|
|
|
||||||
|
|
@ -400,38 +400,30 @@ class TeamsbotService:
|
||||||
if len(audioBytes) < 1000:
|
if len(audioBytes) < 1000:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Use the existing Google Cloud Speech connector for STT
|
if not voiceInterface:
|
||||||
speechConnector = voiceInterface.getSpeechConnector() if voiceInterface else None
|
logger.warning(f"[AudioChunk] No voice interface available for session {sessionId}")
|
||||||
if not speechConnector or not hasattr(speechConnector, 'speech_client'):
|
|
||||||
logger.warning(f"[AudioChunk] No speech client available for session {sessionId}")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
from google.cloud import speech
|
sttResult = await voiceInterface.speechToText(
|
||||||
config = speech.RecognitionConfig(
|
audioContent=audioBytes,
|
||||||
encoding=speech.RecognitionConfig.AudioEncoding.LINEAR16,
|
language=self.config.language or "de-DE",
|
||||||
sample_rate_hertz=sampleRate,
|
sampleRate=sampleRate,
|
||||||
language_code=self.config.language or "de-DE",
|
|
||||||
enable_automatic_punctuation=True,
|
|
||||||
)
|
)
|
||||||
audio = speech.RecognitionAudio(content=audioBytes)
|
|
||||||
|
|
||||||
response = speechConnector.speech_client.recognize(config=config, audio=audio)
|
if sttResult and sttResult.get("success") and sttResult.get("text"):
|
||||||
|
text = sttResult["text"].strip()
|
||||||
for result in response.results:
|
if text:
|
||||||
if result.alternatives:
|
logger.info(f"[AudioChunk] STT result: {text[:80]}...")
|
||||||
text = result.alternatives[0].transcript.strip()
|
await self._processTranscript(
|
||||||
if text:
|
sessionId=sessionId,
|
||||||
logger.info(f"[AudioChunk] STT result: {text[:80]}...")
|
speaker="Meeting Audio",
|
||||||
await self._processTranscript(
|
text=text,
|
||||||
sessionId=sessionId,
|
isFinal=True,
|
||||||
speaker="Meeting Audio",
|
interface=interface,
|
||||||
text=text,
|
voiceInterface=voiceInterface,
|
||||||
isFinal=True,
|
websocket=websocket,
|
||||||
interface=interface,
|
source="audioCapture",
|
||||||
voiceInterface=voiceInterface,
|
)
|
||||||
websocket=websocket,
|
|
||||||
source="audioCapture",
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[AudioChunk] STT error for session {sessionId}: {type(e).__name__}: {e}")
|
logger.error(f"[AudioChunk] STT error for session {sessionId}: {type(e).__name__}: {e}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Copyright (c) 2025 Patrick Motsch
|
# Copyright (c) 2025 Patrick Motsch
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
"""Trustee models: TrusteeOrganisation, TrusteeRole, TrusteeAccess, TrusteeContract, TrusteeDocument, TrusteePosition, TrusteePositionDocument."""
|
"""Trustee models: TrusteeOrganisation, TrusteeRole, TrusteeAccess, TrusteeContract, TrusteeDocument, TrusteePosition."""
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
@ -384,9 +384,8 @@ registerModelLabels(
|
||||||
class TrusteePosition(BaseModel):
|
class TrusteePosition(BaseModel):
|
||||||
"""Contains booking positions (expense entries).
|
"""Contains booking positions (expense entries).
|
||||||
|
|
||||||
Note: organisationId and contractId removed as per architecture decision:
|
Each position references exactly one source document via documentId (1:N relationship).
|
||||||
- The feature instance IS the organisation
|
One document (e.g. bank statement) can generate many positions.
|
||||||
- Contracts are eliminated from the model
|
|
||||||
"""
|
"""
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
|
|
@ -397,6 +396,16 @@ class TrusteePosition(BaseModel):
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
documentId: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Reference to TrusteeDocument.id (source document that generated this position)",
|
||||||
|
json_schema_extra={
|
||||||
|
"frontend_type": "select",
|
||||||
|
"frontend_readonly": False,
|
||||||
|
"frontend_required": False,
|
||||||
|
"frontend_options": "/api/trustee/{instanceId}/documents/options"
|
||||||
|
}
|
||||||
|
)
|
||||||
valuta: Optional[str] = Field(
|
valuta: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Value date (ISO format: YYYY-MM-DD)",
|
description="Value date (ISO format: YYYY-MM-DD)",
|
||||||
|
|
@ -538,6 +547,7 @@ registerModelLabels(
|
||||||
{"en": "Position", "fr": "Position", "de": "Position"},
|
{"en": "Position", "fr": "Position", "de": "Position"},
|
||||||
{
|
{
|
||||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
||||||
|
"documentId": {"en": "Document", "fr": "Document", "de": "Dokument"},
|
||||||
"valuta": {"en": "Value Date", "fr": "Date de valeur", "de": "Valutadatum"},
|
"valuta": {"en": "Value Date", "fr": "Date de valeur", "de": "Valutadatum"},
|
||||||
"transactionDateTime": {"en": "Transaction Date/Time", "fr": "Date/Heure de transaction", "de": "Transaktionszeitpunkt"},
|
"transactionDateTime": {"en": "Transaction Date/Time", "fr": "Date/Heure de transaction", "de": "Transaktionszeitpunkt"},
|
||||||
"company": {"en": "Company", "fr": "Entreprise", "de": "Firma"},
|
"company": {"en": "Company", "fr": "Entreprise", "de": "Firma"},
|
||||||
|
|
@ -555,71 +565,3 @@ registerModelLabels(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TrusteePositionDocument(BaseModel):
|
|
||||||
"""Cross-reference table linking positions to documents (many-to-many).
|
|
||||||
|
|
||||||
Note: organisationId and contractId removed as per architecture decision:
|
|
||||||
- The feature instance IS the organisation
|
|
||||||
- Contracts are eliminated from the model
|
|
||||||
"""
|
|
||||||
id: str = Field(
|
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
|
||||||
description="Unique link ID",
|
|
||||||
json_schema_extra={
|
|
||||||
"frontend_type": "text",
|
|
||||||
"frontend_readonly": True,
|
|
||||||
"frontend_required": False
|
|
||||||
}
|
|
||||||
)
|
|
||||||
documentId: str = Field(
|
|
||||||
description="Reference to TrusteeDocument.id",
|
|
||||||
json_schema_extra={
|
|
||||||
"frontend_type": "select",
|
|
||||||
"frontend_readonly": False,
|
|
||||||
"frontend_required": True,
|
|
||||||
"frontend_options": "/api/trustee/{instanceId}/documents/options"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
positionId: str = Field(
|
|
||||||
description="Reference to TrusteePosition.id",
|
|
||||||
json_schema_extra={
|
|
||||||
"frontend_type": "select",
|
|
||||||
"frontend_readonly": False,
|
|
||||||
"frontend_required": True,
|
|
||||||
"frontend_options": "/api/trustee/{instanceId}/positions/options"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
mandateId: Optional[str] = Field(
|
|
||||||
default=None,
|
|
||||||
description="Mandate ID (auto-set from context)",
|
|
||||||
json_schema_extra={
|
|
||||||
"frontend_type": "text",
|
|
||||||
"frontend_readonly": True,
|
|
||||||
"frontend_required": False,
|
|
||||||
"frontend_hidden": True
|
|
||||||
}
|
|
||||||
)
|
|
||||||
featureInstanceId: Optional[str] = Field(
|
|
||||||
default=None,
|
|
||||||
description="Feature Instance ID for instance-level isolation (auto-set from context)",
|
|
||||||
json_schema_extra={
|
|
||||||
"frontend_type": "text",
|
|
||||||
"frontend_readonly": True,
|
|
||||||
"frontend_required": False,
|
|
||||||
"frontend_hidden": True
|
|
||||||
}
|
|
||||||
)
|
|
||||||
# System attributes are automatically set by DatabaseConnector
|
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"TrusteePositionDocument",
|
|
||||||
{"en": "Position-Document Link", "fr": "Lien Position-Document", "de": "Position-Dokument-Verknüpfung"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
|
||||||
"documentId": {"en": "Document", "fr": "Document", "de": "Dokument"},
|
|
||||||
"positionId": {"en": "Position", "fr": "Position", "de": "Position"},
|
|
||||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ from .datamodelFeatureTrustee import (
|
||||||
TrusteeContract,
|
TrusteeContract,
|
||||||
TrusteeDocument,
|
TrusteeDocument,
|
||||||
TrusteePosition,
|
TrusteePosition,
|
||||||
TrusteePositionDocument,
|
|
||||||
)
|
)
|
||||||
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResult
|
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResult
|
||||||
|
|
||||||
|
|
@ -1107,10 +1106,8 @@ class TrusteeObjects:
|
||||||
def deleteDocument(self, documentId: str) -> bool:
|
def deleteDocument(self, documentId: str) -> bool:
|
||||||
"""Delete a document.
|
"""Delete a document.
|
||||||
|
|
||||||
All position-document cross-table entries (TrusteePositionDocument) referencing
|
Positions referencing this document will have their documentId set to None.
|
||||||
this document are deleted first, then the document.
|
|
||||||
"""
|
"""
|
||||||
# Get existing document to check creator
|
|
||||||
existingRecords = self.db.getRecordset(TrusteeDocument, recordFilter={"id": documentId})
|
existingRecords = self.db.getRecordset(TrusteeDocument, recordFilter={"id": documentId})
|
||||||
existing = existingRecords[0] if existingRecords else None
|
existing = existingRecords[0] if existingRecords else None
|
||||||
|
|
||||||
|
|
@ -1120,12 +1117,17 @@ class TrusteeObjects:
|
||||||
|
|
||||||
createdBy = existing.get("_createdBy")
|
createdBy = existing.get("_createdBy")
|
||||||
|
|
||||||
# Check system RBAC permission (userreport can only delete their own records)
|
|
||||||
if not self.checkCombinedPermission(TrusteeDocument, "delete", recordCreatedBy=createdBy):
|
if not self.checkCombinedPermission(TrusteeDocument, "delete", recordCreatedBy=createdBy):
|
||||||
logger.warning(f"User {self.userId} lacks permission to delete document")
|
logger.warning(f"User {self.userId} lacks permission to delete document")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self._deletePositionDocumentLinksForDocument(documentId)
|
# Clear documentId on positions that reference this document
|
||||||
|
positions = self.db.getRecordset(TrusteePosition, recordFilter={"documentId": documentId})
|
||||||
|
for pos in positions:
|
||||||
|
posId = pos.get("id")
|
||||||
|
if posId:
|
||||||
|
self.db.recordModify(TrusteePosition, posId, {"documentId": None})
|
||||||
|
|
||||||
return self.db.recordDelete(TrusteeDocument, documentId)
|
return self.db.recordDelete(TrusteeDocument, documentId)
|
||||||
|
|
||||||
# ===== Position CRUD =====
|
# ===== Position CRUD =====
|
||||||
|
|
@ -1276,12 +1278,7 @@ class TrusteeObjects:
|
||||||
return TrusteePosition(**{k: v for k, v in updatedRecord.items() if not k.startswith("_")})
|
return TrusteePosition(**{k: v for k, v in updatedRecord.items() if not k.startswith("_")})
|
||||||
|
|
||||||
def deletePosition(self, positionId: str) -> bool:
|
def deletePosition(self, positionId: str) -> bool:
|
||||||
"""Delete a position.
|
"""Delete a position."""
|
||||||
|
|
||||||
All position-document cross-table entries (TrusteePositionDocument) referencing
|
|
||||||
this position are deleted first, then the position.
|
|
||||||
"""
|
|
||||||
# Get existing position to check creator
|
|
||||||
existingRecords = self.db.getRecordset(TrusteePosition, recordFilter={"id": positionId})
|
existingRecords = self.db.getRecordset(TrusteePosition, recordFilter={"id": positionId})
|
||||||
existing = existingRecords[0] if existingRecords else None
|
existing = existingRecords[0] if existingRecords else None
|
||||||
|
|
||||||
|
|
@ -1291,177 +1288,27 @@ class TrusteeObjects:
|
||||||
|
|
||||||
createdBy = existing.get("_createdBy")
|
createdBy = existing.get("_createdBy")
|
||||||
|
|
||||||
# Check system RBAC permission (userreport can only delete their own records)
|
|
||||||
if not self.checkCombinedPermission(TrusteePosition, "delete", recordCreatedBy=createdBy):
|
if not self.checkCombinedPermission(TrusteePosition, "delete", recordCreatedBy=createdBy):
|
||||||
logger.warning(f"User {self.userId} lacks permission to delete position")
|
logger.warning(f"User {self.userId} lacks permission to delete position")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self._deletePositionDocumentLinksForPosition(positionId)
|
|
||||||
return self.db.recordDelete(TrusteePosition, positionId)
|
return self.db.recordDelete(TrusteePosition, positionId)
|
||||||
|
|
||||||
# ===== Position-Document Link CRUD =====
|
# ===== Position-Document Queries =====
|
||||||
|
|
||||||
def createPositionDocument(self, data: Dict[str, Any]) -> Optional[TrusteePositionDocument]:
|
def getPositionsByDocument(self, documentId: str) -> List[TrusteePosition]:
|
||||||
"""Create a new position-document link.
|
"""Get all positions that reference a specific document (1:N via documentId FK)."""
|
||||||
|
|
||||||
Note: organisationId and contractId removed - feature instance IS the organisation.
|
|
||||||
Permission is checked via system RBAC (feature-level access).
|
|
||||||
"""
|
|
||||||
# Check system RBAC permission
|
|
||||||
if not self.checkCombinedPermission(TrusteePositionDocument, "create"):
|
|
||||||
logger.warning(f"User {self.userId} lacks permission to create position-document link")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Auto-set context fields
|
|
||||||
data["mandateId"] = self.mandateId
|
|
||||||
data["featureInstanceId"] = self.featureInstanceId
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
linkId = data.get("id") or str(uuid.uuid4())
|
|
||||||
data["id"] = linkId
|
|
||||||
|
|
||||||
createdRecord = self.db.recordCreate(TrusteePositionDocument, data)
|
|
||||||
if createdRecord and createdRecord.get("id"):
|
|
||||||
return TrusteePositionDocument(**{k: v for k, v in createdRecord.items() if not k.startswith("_")})
|
|
||||||
return None
|
|
||||||
|
|
||||||
def getPositionDocument(self, linkId: str) -> Optional[TrusteePositionDocument]:
|
|
||||||
"""Get a single position-document link by ID."""
|
|
||||||
records = self.db.getRecordset(TrusteePositionDocument, recordFilter={"id": linkId})
|
|
||||||
if not records:
|
|
||||||
return None
|
|
||||||
return TrusteePositionDocument(**{k: v for k, v in records[0].items() if not k.startswith("_")})
|
|
||||||
|
|
||||||
def updatePositionDocument(self, linkId: str, data: Dict[str, Any]) -> Optional[TrusteePositionDocument]:
|
|
||||||
"""Update a position-document link."""
|
|
||||||
# Check permission
|
|
||||||
if not self.checkCombinedPermission(TrusteePositionDocument, "update"):
|
|
||||||
logger.warning(f"User {self.userId} lacks permission to update position-document link")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Verify link exists and belongs to this instance
|
|
||||||
existing = self.db.getRecordset(TrusteePositionDocument, recordFilter={"id": linkId})
|
|
||||||
if not existing:
|
|
||||||
logger.warning(f"Position-document link {linkId} not found")
|
|
||||||
return None
|
|
||||||
|
|
||||||
existingRecord = existing[0]
|
|
||||||
if existingRecord.get("featureInstanceId") != self.featureInstanceId:
|
|
||||||
logger.warning(f"Link {linkId} belongs to different instance")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Prevent changing context fields
|
|
||||||
data.pop("id", None)
|
|
||||||
data.pop("mandateId", None)
|
|
||||||
data.pop("featureInstanceId", None)
|
|
||||||
|
|
||||||
updatedRecord = self.db.recordModify(TrusteePositionDocument, linkId, data)
|
|
||||||
if updatedRecord and updatedRecord.get("id"):
|
|
||||||
return TrusteePositionDocument(**{k: v for k, v in updatedRecord.items() if not k.startswith("_")})
|
|
||||||
return None
|
|
||||||
|
|
||||||
def getAllPositionDocuments(self, params: Optional[PaginationParams] = None) -> PaginatedResult:
|
|
||||||
"""Get all position-document links with RBAC filtering + feature-level access filtering."""
|
|
||||||
# Step 1: System RBAC filtering with per-row permissions
|
|
||||||
records = getRecordsetWithRBAC(
|
records = getRecordsetWithRBAC(
|
||||||
connector=self.db,
|
connector=self.db,
|
||||||
modelClass=TrusteePositionDocument,
|
modelClass=TrusteePosition,
|
||||||
currentUser=self.currentUser,
|
|
||||||
recordFilter=None,
|
|
||||||
orderBy="id",
|
|
||||||
mandateId=self.mandateId,
|
|
||||||
featureInstanceId=self.featureInstanceId,
|
|
||||||
enrichPermissions=True,
|
|
||||||
featureCode=self.FEATURE_CODE
|
|
||||||
)
|
|
||||||
|
|
||||||
totalItems = len(records)
|
|
||||||
if params:
|
|
||||||
pageSize = params.pageSize or 20
|
|
||||||
page = params.page or 1
|
|
||||||
startIdx = (page - 1) * pageSize
|
|
||||||
endIdx = startIdx + pageSize
|
|
||||||
items = records[startIdx:endIdx]
|
|
||||||
totalPages = math.ceil(totalItems / pageSize) if pageSize > 0 else 1
|
|
||||||
else:
|
|
||||||
items = records
|
|
||||||
totalPages = 1
|
|
||||||
page = 1
|
|
||||||
pageSize = totalItems
|
|
||||||
|
|
||||||
return PaginatedResult(
|
|
||||||
items=items,
|
|
||||||
totalItems=totalItems,
|
|
||||||
totalPages=totalPages
|
|
||||||
)
|
|
||||||
|
|
||||||
def getDocumentsForPosition(self, positionId: str) -> List[TrusteePositionDocument]:
|
|
||||||
"""Get all documents linked to a position."""
|
|
||||||
# Step 1: System RBAC filtering
|
|
||||||
links = getRecordsetWithRBAC(
|
|
||||||
connector=self.db,
|
|
||||||
modelClass=TrusteePositionDocument,
|
|
||||||
currentUser=self.currentUser,
|
|
||||||
recordFilter={"positionId": positionId},
|
|
||||||
orderBy="id",
|
|
||||||
mandateId=self.mandateId,
|
|
||||||
featureInstanceId=self.featureInstanceId,
|
|
||||||
featureCode=self.FEATURE_CODE
|
|
||||||
)
|
|
||||||
return [TrusteePositionDocument(**{k: v for k, v in r.items() if not k.startswith("_")}) for r in links]
|
|
||||||
|
|
||||||
def getPositionsForDocument(self, documentId: str) -> List[TrusteePositionDocument]:
|
|
||||||
"""Get all positions linked to a document."""
|
|
||||||
# Step 1: System RBAC filtering
|
|
||||||
links = getRecordsetWithRBAC(
|
|
||||||
connector=self.db,
|
|
||||||
modelClass=TrusteePositionDocument,
|
|
||||||
currentUser=self.currentUser,
|
currentUser=self.currentUser,
|
||||||
recordFilter={"documentId": documentId},
|
recordFilter={"documentId": documentId},
|
||||||
orderBy="id",
|
orderBy="valuta",
|
||||||
mandateId=self.mandateId,
|
mandateId=self.mandateId,
|
||||||
featureInstanceId=self.featureInstanceId,
|
featureInstanceId=self.featureInstanceId,
|
||||||
featureCode=self.FEATURE_CODE
|
featureCode=self.FEATURE_CODE
|
||||||
)
|
)
|
||||||
return [TrusteePositionDocument(**{k: v for k, v in r.items() if not k.startswith("_")}) for r in links]
|
return [TrusteePosition(**{k: v for k, v in r.items() if not k.startswith("_")}) for r in records]
|
||||||
|
|
||||||
def deletePositionDocument(self, linkId: str) -> bool:
|
|
||||||
"""Delete a position-document link.
|
|
||||||
|
|
||||||
Note: organisationId and contractId removed - feature instance IS the organisation.
|
|
||||||
"""
|
|
||||||
# Get existing link to check creator
|
|
||||||
existingRecords = self.db.getRecordset(TrusteePositionDocument, recordFilter={"id": linkId})
|
|
||||||
existing = existingRecords[0] if existingRecords else None
|
|
||||||
|
|
||||||
if not existing:
|
|
||||||
logger.warning(f"Position-document link {linkId} not found")
|
|
||||||
return False
|
|
||||||
|
|
||||||
createdBy = existing.get("_createdBy")
|
|
||||||
|
|
||||||
# Check system RBAC permission (userreport can only delete their own records)
|
|
||||||
if not self.checkCombinedPermission(TrusteePositionDocument, "delete", recordCreatedBy=createdBy):
|
|
||||||
logger.warning(f"User {self.userId} lacks permission to delete position-document link")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return self.db.recordDelete(TrusteePositionDocument, linkId)
|
|
||||||
|
|
||||||
def _deletePositionDocumentLinksForDocument(self, documentId: str) -> None:
|
|
||||||
"""Delete all position-document cross-table entries referencing this document."""
|
|
||||||
links = self.db.getRecordset(TrusteePositionDocument, recordFilter={"documentId": documentId})
|
|
||||||
for link in links:
|
|
||||||
linkId = link.get("id")
|
|
||||||
if linkId:
|
|
||||||
self.db.recordDelete(TrusteePositionDocument, linkId)
|
|
||||||
|
|
||||||
def _deletePositionDocumentLinksForPosition(self, positionId: str) -> None:
|
|
||||||
"""Delete all position-document cross-table entries referencing this position."""
|
|
||||||
links = self.db.getRecordset(TrusteePositionDocument, recordFilter={"positionId": positionId})
|
|
||||||
for link in links:
|
|
||||||
linkId = link.get("id")
|
|
||||||
if linkId:
|
|
||||||
self.db.recordDelete(TrusteePositionDocument, linkId)
|
|
||||||
|
|
||||||
# ===== Trustee-specific Access Check =====
|
# ===== Trustee-specific Access Check =====
|
||||||
|
|
||||||
|
|
@ -1583,7 +1430,7 @@ class TrusteeObjects:
|
||||||
|
|
||||||
# operate role: CRUD for contracts, documents, positions
|
# operate role: CRUD for contracts, documents, positions
|
||||||
if "operate" in roles:
|
if "operate" in roles:
|
||||||
if modelClass in (TrusteeContract, TrusteeDocument, TrusteePosition, TrusteePositionDocument):
|
if modelClass in (TrusteeContract, TrusteeDocument, TrusteePosition):
|
||||||
return True
|
return True
|
||||||
# operate can read organisations
|
# operate can read organisations
|
||||||
if modelClass == TrusteeOrganisation and operation == "read":
|
if modelClass == TrusteeOrganisation and operation == "read":
|
||||||
|
|
@ -1591,7 +1438,7 @@ class TrusteeObjects:
|
||||||
|
|
||||||
# userreport role: CRUD own records for documents/positions
|
# userreport role: CRUD own records for documents/positions
|
||||||
if "userreport" in roles:
|
if "userreport" in roles:
|
||||||
if modelClass in (TrusteeDocument, TrusteePosition, TrusteePositionDocument):
|
if modelClass in (TrusteeDocument, TrusteePosition):
|
||||||
# For create, always allowed
|
# For create, always allowed
|
||||||
if operation == "create":
|
if operation == "create":
|
||||||
return True
|
return True
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,6 @@ UI_OBJECTS = [
|
||||||
"label": {"en": "Documents", "de": "Dokumente", "fr": "Documents"},
|
"label": {"en": "Documents", "de": "Dokumente", "fr": "Documents"},
|
||||||
"meta": {"area": "documents"}
|
"meta": {"area": "documents"}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"objectKey": "ui.feature.trustee.position-documents",
|
|
||||||
"label": {"en": "Position Documents", "de": "Positions-Dokumente", "fr": "Documents de position"},
|
|
||||||
"meta": {"area": "position-documents"}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.trustee.expense-import",
|
"objectKey": "ui.feature.trustee.expense-import",
|
||||||
"label": {"en": "Expense Import", "de": "Spesen Import", "fr": "Import de dépenses"},
|
"label": {"en": "Expense Import", "de": "Spesen Import", "fr": "Import de dépenses"},
|
||||||
|
|
@ -63,11 +58,6 @@ DATA_OBJECTS = [
|
||||||
"label": {"en": "Document", "de": "Dokument", "fr": "Document"},
|
"label": {"en": "Document", "de": "Dokument", "fr": "Document"},
|
||||||
"meta": {"table": "TrusteeDocument", "fields": ["id", "filename", "mimeType", "fileSize", "uploadDate"]}
|
"meta": {"table": "TrusteeDocument", "fields": ["id", "filename", "mimeType", "fileSize", "uploadDate"]}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"objectKey": "data.feature.trustee.TrusteePositionDocument",
|
|
||||||
"label": {"en": "Position-Document Assignment", "de": "Position-Dokument Zuordnung", "fr": "Assignation Position-Document"},
|
|
||||||
"meta": {"table": "TrusteePositionDocument", "fields": ["id", "positionId", "documentId"]}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.trustee.*",
|
"objectKey": "data.feature.trustee.*",
|
||||||
"label": {"en": "All Trustee Data", "de": "Alle Treuhand-Daten", "fr": "Toutes les données fiduciaires"},
|
"label": {"en": "All Trustee Data", "de": "Alle Treuhand-Daten", "fr": "Toutes les données fiduciaires"},
|
||||||
|
|
@ -148,7 +138,6 @@ TEMPLATE_ROLES = [
|
||||||
{"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True},
|
{"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.trustee.positions", "view": True},
|
{"context": "UI", "item": "ui.feature.trustee.positions", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.trustee.documents", "view": True},
|
{"context": "UI", "item": "ui.feature.trustee.documents", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.trustee.position-documents", "view": True},
|
|
||||||
# Group-level DATA access
|
# Group-level DATA access
|
||||||
{"context": "DATA", "item": None, "view": True, "read": "g", "create": "g", "update": "g", "delete": "g"},
|
{"context": "DATA", "item": None, "view": True, "read": "g", "create": "g", "update": "g", "delete": "g"},
|
||||||
]
|
]
|
||||||
|
|
@ -165,12 +154,10 @@ TEMPLATE_ROLES = [
|
||||||
{"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True},
|
{"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.trustee.positions", "view": True},
|
{"context": "UI", "item": "ui.feature.trustee.positions", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.trustee.documents", "view": True},
|
{"context": "UI", "item": "ui.feature.trustee.documents", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.trustee.position-documents", "view": True},
|
|
||||||
{"context": "UI", "item": "ui.feature.trustee.expense-import", "view": True},
|
{"context": "UI", "item": "ui.feature.trustee.expense-import", "view": True},
|
||||||
# Own records only (MY level) - explizite Regeln pro Tabelle
|
# Own records only (MY level)
|
||||||
{"context": "DATA", "item": "data.feature.trustee.TrusteePosition", "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
|
{"context": "DATA", "item": "data.feature.trustee.TrusteePosition", "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
|
||||||
{"context": "DATA", "item": "data.feature.trustee.TrusteeDocument", "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
|
{"context": "DATA", "item": "data.feature.trustee.TrusteeDocument", "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
|
||||||
{"context": "DATA", "item": "data.feature.trustee.TrusteePositionDocument", "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue