hotfixes for work with nyla
This commit is contained in:
parent
be14c6c84d
commit
58b13ff7c6
16 changed files with 2848 additions and 60 deletions
3
app.py
3
app.py
|
|
@ -455,3 +455,6 @@ app.include_router(messagingRouter)
|
|||
from modules.routes.routeChatbot import router as chatbotRouter
|
||||
app.include_router(chatbotRouter)
|
||||
|
||||
from modules.routes.routeDataTrustee import router as trusteeRouter
|
||||
app.include_router(trusteeRouter)
|
||||
|
||||
|
|
|
|||
|
|
@ -59,10 +59,12 @@ def _get_model_fields(model_class) -> Dict[str, str]:
|
|||
hasattr(field_type, "__origin__")
|
||||
and field_type.__origin__ in (dict, list)
|
||||
)
|
||||
# Check if field type is a Pydantic BaseModel (for nested models)
|
||||
# Check if field type is directly a Pydantic BaseModel subclass (for nested models like TextMultilingual)
|
||||
or (isinstance(field_type, type) and issubclass(field_type, BaseModel))
|
||||
# Check if field type is Optional[BaseModel] (Union with None)
|
||||
or (hasattr(field_type, "__origin__") and get_origin(field_type) is Union
|
||||
and any(hasattr(arg, "__bases__") and BaseModel in getattr(arg, "__bases__", ())
|
||||
for arg in get_args(field_type)))
|
||||
and any(isinstance(arg, type) and issubclass(arg, BaseModel)
|
||||
for arg in get_args(field_type) if arg is not type(None)))
|
||||
):
|
||||
fields[field_name] = "JSONB"
|
||||
# Simple type mapping
|
||||
|
|
|
|||
314
modules/datamodels/FIELD_NAMES.md
Normal file
314
modules/datamodels/FIELD_NAMES.md
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
| Field Name | Type Pattern | Models Using It |
|
||||
|------------|--------------|-----------------|
|
||||
| `accumulatedJsonString` | str | JsonAccumulationState |
|
||||
| `action` | str | ActionDefinition |
|
||||
| `actionId` | str | ChatDocument, ChatMessage |
|
||||
| `actionList` | List | TaskItem |
|
||||
| `actionMethod` | str | ChatMessage |
|
||||
| `actionName` | str | ChatMessage |
|
||||
| `actionNumber` | int | ChatLog, ChatDocument, ChatMessage |
|
||||
| `actionObjective` | str | ActionDefinition, TaskContext |
|
||||
| `actionProgress` | str | ChatMessage |
|
||||
| `actionResult` | Any | TaskResult |
|
||||
| `active` | bool | AutomationDefinition |
|
||||
| `additionalData` | Dict | AiResponseMetadata |
|
||||
| `aiPrompt` | str | AiProcessParameters |
|
||||
| `allSections` | List | JsonAccumulationState |
|
||||
| `apiUrl` | str | AiModel |
|
||||
| `authenticationAuthority` | AuthAuthority | User |
|
||||
| `authority` | AuthAuthority | UserConnection, Token |
|
||||
| `availableConnections` | list | TaskContext |
|
||||
| `availableDocuments` | str | TaskContext |
|
||||
| `base64Encoded` | bool | ContentMetadata, FileData |
|
||||
| `bytesSent` | int | ChatStat, AiCallResponse |
|
||||
| `bytesReceived` | int | ChatStat, AiCallResponse |
|
||||
| `classes` | List | CodeContentPromptArgs |
|
||||
| `colorMode` | str | ContentMetadata |
|
||||
| `compressContext` | bool | AiCallOptions |
|
||||
| `compressPrompt` | bool | AiCallOptions |
|
||||
| `condition` | str | SelectionRule |
|
||||
| `confidence` | float | ReviewResult |
|
||||
| `connectedAt` | float | UserConnection |
|
||||
| `connectionId` | str | Token |
|
||||
| `connectionReference` | str | ActionDefinition |
|
||||
| `connectorType` | str | AiModel |
|
||||
| `content` | str | Prompt, ContentItem, AiResponse, AiCallResponse, FilePreview |
|
||||
| `contentAnalysis` | Dict | Observation |
|
||||
| `contentParts` | List | AiCallRequest, AiProcessParameters, SectionPromptArgs, ChapterStructurePromptArgs, CodeContentPromptArgs, CodeStructurePromptArgs |
|
||||
| `contentSize` | str | ObservationPreview |
|
||||
| `contentValidation` | Dict | Observation |
|
||||
| `contents` | List | ChatContentExtracted |
|
||||
| `context` | Dict, AccessRuleContext | TaskHandover, UnderstandingResult, AccessRule |
|
||||
| `contextInfo` | str | CodeContentPromptArgs |
|
||||
| `costPer1kTokensInput` | float | AiModel |
|
||||
| `costPer1kTokensOutput` | float | AiModel |
|
||||
| `country` | str | AiCallPromptWebSearch |
|
||||
| `create` | AccessLevel | AccessRule, UserPermissions |
|
||||
| `created` | str | ObservationPreview |
|
||||
| `createdAt` | float | Token |
|
||||
| `creationDate` | float | FileItem |
|
||||
| `criteriaProgress` | dict | TaskContext |
|
||||
| `currentAction` | int | ChatWorkflow |
|
||||
| `currentRound` | int | ChatWorkflow |
|
||||
| `currentTask` | int | ChatWorkflow |
|
||||
| `data` | str | ContentItem, FileData |
|
||||
| `dataType` | str | TaskStep |
|
||||
| `delete` | AccessLevel | AccessRule, UserPermissions |
|
||||
| `deliverable` | Dict | TaskDefinition |
|
||||
| `delivered_summary` | str | ContinuationContext |
|
||||
| `dependencies` | List | TaskItem, TaskStep, CodeContentPromptArgs |
|
||||
| `description` | TextMultilingual | Role |
|
||||
| `details` | str | AuthEvent |
|
||||
| `detectedComplexity` | str | RequestContext |
|
||||
| `displayName` | str | AiModel |
|
||||
| `documentData` | Any | ActionDocument, DocumentData |
|
||||
| `documentId` | str | ObservationPreview |
|
||||
| `documentList` | DocumentReferenceList | ActionDefinition, ExtractContentParameters |
|
||||
| `documentName` | str | ActionDocument, DocumentData |
|
||||
| `documentReferences` | List | UnderstandingResult |
|
||||
| `documents` | List | ChatMessage, ActionResult, AiResponse |
|
||||
| `documentsCount` | int | Observation |
|
||||
| `documentsLabel` | str | ChatMessage, DocumentExchange |
|
||||
| `durationSec` | float | ContentMetadata |
|
||||
| `email` | EmailStr | User |
|
||||
| `enabled` | bool | Mandate, User |
|
||||
| `encoding` | str | FilePreview |
|
||||
| `engine` | str | ChatStat |
|
||||
| `error` | str | ContentMetadata, ActionResult, ActionItem, TaskItem, TaskResult |
|
||||
| `errorCount` | int | ChatStat, AiCallResponse |
|
||||
| `estimatedComplexity` | str | TaskStep |
|
||||
| `eventId` | str | AutomationDefinition |
|
||||
| `eventType` | str | AuthEvent |
|
||||
| `execAction` | str | ActionItem |
|
||||
| `execMethod` | str | ActionItem |
|
||||
| `execParameters` | Dict | ActionItem |
|
||||
| `execResultLabel` | str | ActionItem |
|
||||
| `executedActions` | list | TaskContext |
|
||||
| `executionLogs` | List | AutomationDefinition |
|
||||
| `expectedDocumentFormats` | List | ActionItem |
|
||||
| `expectedFormats` | List | ChatWorkflow, TaskStep |
|
||||
| `expectedOutputFormat` | str | RequestContext |
|
||||
| `expectedOutputType` | str | RequestContext |
|
||||
| `expiresAt` | float | UserConnection, Token |
|
||||
| `externalEmail` | EmailStr | UserConnection |
|
||||
| `externalId` | str | UserConnection |
|
||||
| `externalUsername` | str | UserConnection |
|
||||
| `extractionMethod` | str | AiResponseMetadata |
|
||||
| `extractionOptions` | Any | TaskDefinition, ExtractContentParameters |
|
||||
| `failedActions` | list | TaskContext |
|
||||
| `failurePatterns` | list | TaskContext |
|
||||
| `feedback` | str | TaskResult, TaskItem |
|
||||
| `fileHash` | str | FileItem |
|
||||
| `fileId` | str | ChatDocument |
|
||||
| `fileName` | str | FileItem, FilePreview, ChatDocument |
|
||||
| `fileSize` | int | FileItem, ChatDocument |
|
||||
| `fileType` | str | CodeContentPromptArgs |
|
||||
| `filename` | str | AiResponseMetadata, CodeContentPromptArgs |
|
||||
| `finishedAt` | float | TaskItem |
|
||||
| `fps` | float | ContentMetadata |
|
||||
| `fullName` | str | User |
|
||||
| `functionCall` | Callable | AiModel |
|
||||
| `functions` | List | CodeContentPromptArgs |
|
||||
| `generationHint` | str | SectionPromptArgs |
|
||||
| `handoverType` | str | TaskHandover |
|
||||
| `hashedPassword` | str | UserInDB |
|
||||
| `height` | int | ContentMetadata |
|
||||
| `hierarchyContext` | str | JsonContinuationContexts |
|
||||
| `hierarchyContextForPrompt` | str | JsonContinuationContexts |
|
||||
| `id` | str | *Most models* |
|
||||
| `improvements` | List | TaskHandover, TaskContext, ReviewResult |
|
||||
| `incomplete_part` | str | ContinuationContext |
|
||||
| `inputDocuments` | List | TaskHandover |
|
||||
| `instruction` | str | AiCallPromptWebSearch, AiCallPromptWebCrawl |
|
||||
| `intention` | Dict | UnderstandingResult |
|
||||
| `ipAddress` | str | AuthEvent |
|
||||
| `isAccumulationMode` | bool | JsonAccumulationState |
|
||||
| `isAggregation` | bool | SectionPromptArgs |
|
||||
| `isAvailable` | bool | AiModel |
|
||||
| `isRegeneration` | bool | TaskContext |
|
||||
| `isSystemRole` | bool | Role |
|
||||
| `isText` | bool | FilePreview |
|
||||
| `item` | str | AccessRule |
|
||||
| `jsonParsingSuccess` | bool | JsonContinuationContexts |
|
||||
| `kpis` | List | JsonAccumulationState |
|
||||
| `label` | str | ContentItem, AutomationDefinition |
|
||||
| `language` | str | Mandate, User, SectionPromptArgs, AiCallPromptWebSearch |
|
||||
| `last_complete_part` | str | ContinuationContext |
|
||||
| `last_raw_json` | str | ContinuationContext |
|
||||
| `lastActivity` | float | ChatWorkflow |
|
||||
| `lastChecked` | float | UserConnection |
|
||||
| `lastParsedResult` | Dict | JsonAccumulationState |
|
||||
| `lastUpdated` | str | AiModel |
|
||||
| `learnings` | List | ActionDefinition, TaskContext |
|
||||
| `listFileId` | List | UserInputRequest |
|
||||
| `logs` | List | ChatWorkflow |
|
||||
| `mandateId` | str | ChatWorkflow, FileItem, Prompt, User, AutomationDefinition, Token |
|
||||
| `maxCost` | float | SelectionRule, AiCallOptions |
|
||||
| `maxDepth` | int | AiCallPromptWebCrawl |
|
||||
| `maxNumberPages` | int | AiCallPromptWebSearch |
|
||||
| `maxParts` | int | AiCallOptions |
|
||||
| `maxProcessingTime` | int | AiCallOptions |
|
||||
| `maxSteps` | int | ChatWorkflow |
|
||||
| `maxTokens` | int | AiModel |
|
||||
| `maxWidth` | int | AiCallPromptWebCrawl |
|
||||
| `message` | str | ChatLog, ChatMessage |
|
||||
| `messageHistory` | List | TaskHandover |
|
||||
| `messageId` | str | ChatDocument |
|
||||
| `messages` | List | ChatWorkflow, AiModelCall |
|
||||
| `metadata` | ContentMetadata, AiResponseMetadata, Dict | ContentItem, AiResponse, AiModelResponse, CodeContentPromptArgs |
|
||||
| `metCriteria` | List | ReviewResult |
|
||||
| `method` | str | ActionSelection |
|
||||
| `mime` | str | ObservationPreview |
|
||||
| `mimeType` | str | ContentMetadata, FileItem, FilePreview, ChatDocument, ActionDocument, DocumentData |
|
||||
| `minContextLength` | int | AiModel, SelectionRule |
|
||||
| `minQualityRating` | int | SelectionRule |
|
||||
| `missingOutputs` | List | ReviewResult |
|
||||
| `model` | AiModel | AiModelCall |
|
||||
| `modelId` | str | AiModelResponse |
|
||||
| `modelName` | str | AiCallResponse |
|
||||
| `modified` | str | ObservationPreview |
|
||||
| `name` | str | Mandate, Prompt, ActionSelection, AiModel, SelectionRule, ObservationPreview |
|
||||
| `nextAction` | str | ReviewResult |
|
||||
| `nextActionGuidance` | Dict | TaskContext |
|
||||
| `nextActionObjective` | str | ReviewResult |
|
||||
| `nextActionParameters` | Dict | ReviewResult |
|
||||
| `notes` | List | Observation |
|
||||
| `objective` | str | TaskStep, TaskDefinition |
|
||||
| `operationId` | str | ChatLog |
|
||||
| `operationType` | OperationTypeEnum, str | OperationTypeRating, AiCallOptions, AiResponseMetadata |
|
||||
| `operationTypes` | List | AiModel, SelectionRule |
|
||||
| `options` | AiCallOptions | AiCallRequest, AiModelCall |
|
||||
| `originalPrompt` | str | RequestContext |
|
||||
| `outputDocuments` | List | TaskHandover |
|
||||
| `outputFormat` | str | ChapterStructurePromptArgs |
|
||||
| `overlapContext` | str | JsonContinuationContexts |
|
||||
| `overlap_context` | str | ContinuationContext |
|
||||
| `overview` | str | TaskPlan |
|
||||
| `pages` | int | ContentMetadata |
|
||||
| `parameters` | Dict | ActionParameters, UnderstandingResult, ActionDefinition |
|
||||
| `parametersContext` | str | ActionDefinition, TaskContext |
|
||||
| `parentId` | str | ChatLog |
|
||||
| `parentMessageId` | str | ChatMessage |
|
||||
| `performance` | Dict | ChatLog |
|
||||
| `placeholders` | List, Dict | PromptBundle, AutomationDefinition |
|
||||
| `priceUsd` | float | ChatStat, AiCallResponse |
|
||||
| `previews` | List | Observation |
|
||||
| `previousActionResults` | list | TaskContext |
|
||||
| `previousHandover` | TaskHandover | TaskContext |
|
||||
| `previousResults` | List | TaskHandover, TaskContext, ReviewContext |
|
||||
| `previousReviewResult` | dict | TaskContext |
|
||||
| `priority` | PriorityEnum | AiModel, SelectionRule, AiCallOptions |
|
||||
| `process` | str | ChatStat |
|
||||
| `processDocumentsIndividually` | bool | AiCallOptions |
|
||||
| `processingMode` | ProcessingModeEnum | AiModel, AiCallOptions |
|
||||
| `processingTime` | float | ChatStat, ActionItem, TaskItem, AiCallResponse, AiModelResponse |
|
||||
| `progress` | float | ChatLog |
|
||||
| `prompt` | str | UserInputRequest, PromptBundle, AiCallPromptImage |
|
||||
| `publishedAt` | float | ChatMessage |
|
||||
| `qualityRating` | int | AiModel |
|
||||
| `qualityRequirements` | Dict | TaskStep |
|
||||
| `qualityScore` | float | ReviewResult |
|
||||
| `quality` | str | AiCallPromptImage |
|
||||
| `rating` | int | OperationTypeRating |
|
||||
| `read` | AccessLevel | AccessRule, UserPermissions |
|
||||
| `reason` | str | Token, ReviewResult |
|
||||
| `reference` | str | ObservationPreview |
|
||||
| `requiredDocuments` | List | TaskDefinition |
|
||||
| `requiresAnalysis` | bool | RequestContext |
|
||||
| `requiresContentGeneration` | bool | TaskDefinition |
|
||||
| `requiresDocumentAnalysis` | bool | TaskDefinition |
|
||||
| `requiresDocuments` | bool | RequestContext |
|
||||
| `requiresWebResearch` | bool | RequestContext, TaskDefinition |
|
||||
| `researchDepth` | str | AiCallPromptWebSearch |
|
||||
| `resetToken` | str | UserInDB |
|
||||
| `resetTokenExpires` | float | UserInDB |
|
||||
| `result` | str | ActionItem |
|
||||
| `resultFormat` | str | AiCallOptions |
|
||||
| `resultLabel` | str | ActionResult, Observation |
|
||||
| `resultLabels` | Dict | TaskItem |
|
||||
| `resultType` | str | AiProcessParameters |
|
||||
| `retryCount` | int | ActionItem, TaskItem, TaskContext |
|
||||
| `retryMax` | int | ActionItem, TaskItem |
|
||||
| `revokedAt` | float | Token |
|
||||
| `revokedBy` | str | Token |
|
||||
| `role` | str | ChatMessage |
|
||||
| `roleLabel` | str | Role, AccessRule |
|
||||
| `roleLabels` | List | User |
|
||||
| `rollbackOnFailure` | bool | TaskItem |
|
||||
| `roundNumber` | int | ChatLog, ChatDocument, ChatMessage |
|
||||
| `safetyMargin` | float | AiCallOptions |
|
||||
| `schedule` | str | AutomationDefinition |
|
||||
| `schemaVersion` | str | AiResponseMetadata |
|
||||
| `section` | Dict | SectionPromptArgs |
|
||||
| `section_count` | int | ContinuationContext |
|
||||
| `sectionIndex` | int | SectionPromptArgs |
|
||||
| `sequenceNr` | int | ChatMessage |
|
||||
| `sessionId` | str | Token |
|
||||
| `size` | int, str | ContentMetadata, FilePreview, AiCallPromptImage, ObservationPreview |
|
||||
| `snippet` | str | ObservationPreview |
|
||||
| `sourceDocuments` | List | AiResponseMetadata |
|
||||
| `sourceJson` | Dict | ActionDocument, DocumentData |
|
||||
| `sourceTask` | str | TaskHandover |
|
||||
| `speedRating` | int | AiModel |
|
||||
| `stage1Selection` | dict | TaskContext |
|
||||
| `startedAt` | float | ChatWorkflow, TaskItem |
|
||||
| `stats` | List | ChatWorkflow |
|
||||
| `status` | str, TokenStatus, TaskStatus, ConnectionStatus | ChatLog, ChatWorkflow, ChatMessage, UserConnection, AutomationDefinition, ActionItem, TaskItem, TaskResult, ReviewResult, Token |
|
||||
| `style` | str | AiCallPromptImage |
|
||||
| `success` | bool | ChatMessage, ActionResult, Observation, TaskResult, AuthEvent, AiModelResponse |
|
||||
| `successCriteria` | list | TaskStep |
|
||||
| `successfulActions` | list | TaskContext |
|
||||
| `summary` | str | ChatMessage |
|
||||
| `summaryAllowed` | bool | PromptPlaceholder |
|
||||
| `taskActions` | list | ReviewContext |
|
||||
| `taskId` | str | TaskResult, TaskHandover |
|
||||
| `taskNumber` | int | ChatLog, ChatDocument, ChatMessage |
|
||||
| `taskProgress` | str | ChatMessage |
|
||||
| `tasks` | List | ChatWorkflow, TaskPlan, UnderstandingResult |
|
||||
| `taskStep` | TaskStep | TaskContext, ReviewContext |
|
||||
| `temperature` | float | AiModel, AiCallOptions |
|
||||
| `template` | str | AutomationDefinition |
|
||||
| `template_structure` | str | ContinuationContext |
|
||||
| `timestamp` | float | ChatLog, ActionItem, TaskHandover, AuthEvent |
|
||||
| `title` | str | AiResponseMetadata |
|
||||
| `tokenAccess` | str | Token |
|
||||
| `tokenExpiresAt` | float | UserConnection |
|
||||
| `tokenRefresh` | str | Token |
|
||||
| `tokensUsed` | Dict | AiModelResponse |
|
||||
| `tokenStatus` | str | UserConnection |
|
||||
| `tokenType` | str | Token |
|
||||
| `totalActions` | int | ChatWorkflow |
|
||||
| `totalTasks` | int | ChatWorkflow |
|
||||
| `type` | str | ChatLog |
|
||||
| `typeGroup` | str | ObservationPreview |
|
||||
| `unmetCriteria` | List | ReviewResult |
|
||||
| `update` | AccessLevel | AccessRule, UserPermissions |
|
||||
| `url` | str | AiCallPromptWebCrawl |
|
||||
| `userAgent` | str | AuthEvent |
|
||||
| `userId` | str | UserConnection, Token, AuthEvent |
|
||||
| `userInput` | str | TaskItem |
|
||||
| `userLanguage` | str | UserInputRequest, RequestContext |
|
||||
| `userMessage` | str | ActionItem, TaskStep, ReviewResult, TaskPlan, ActionDefinition |
|
||||
| `userPrompt` | str | SectionPromptArgs, ChapterStructurePromptArgs, CodeContentPromptArgs, CodeStructurePromptArgs |
|
||||
| `username` | str | User |
|
||||
| `validationMetadata` | Dict | ActionDocument |
|
||||
| `version` | str | AiModel |
|
||||
| `view` | bool | AccessRule, UserPermissions |
|
||||
| `weight` | float | SelectionRule |
|
||||
| `width` | int | ContentMetadata |
|
||||
| `workflow` | ChatWorkflow | TaskContext |
|
||||
| `workflowId` | str | ChatStat, ChatLog, ChatMessage, ChatWorkflow, TaskItem, TaskContext, ReviewContext |
|
||||
| `workflowMode` | WorkflowModeEnum | ChatWorkflow |
|
||||
| `workflowSummary` | str | TaskHandover |
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Can you adapt following fields to Multilingual Fields (`TextMultilingual`):
|
||||
|
||||
| Field Name | Models |
|
||||
|------------|--------|
|
||||
| `description` | Role | (is already)
|
||||
|
||||
|
||||
|
|
@ -278,6 +278,7 @@ class WorkflowModeEnum(str, Enum):
|
|||
WORKFLOW_DYNAMIC = "Dynamic"
|
||||
WORKFLOW_AUTOMATION = "Automation"
|
||||
WORKFLOW_CHATBOT = "Chatbot"
|
||||
WORKFLOW_REACT = "React" # Legacy mode - kept for backward compatibility
|
||||
|
||||
|
||||
registerModelLabels(
|
||||
|
|
@ -287,6 +288,7 @@ registerModelLabels(
|
|||
"WORKFLOW_DYNAMIC": {"en": "Dynamic", "fr": "Dynamique"},
|
||||
"WORKFLOW_AUTOMATION": {"en": "Automation", "fr": "Automatisation"},
|
||||
"WORKFLOW_CHATBOT": {"en": "Chatbot", "fr": "Chatbot"},
|
||||
"WORKFLOW_REACT": {"en": "React (Legacy)", "fr": "React (Hérité)"},
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -325,6 +327,10 @@ class ChatWorkflow(BaseModel):
|
|||
"value": WorkflowModeEnum.WORKFLOW_CHATBOT.value,
|
||||
"label": {"en": "Chatbot", "fr": "Chatbot"},
|
||||
},
|
||||
{
|
||||
"value": WorkflowModeEnum.WORKFLOW_REACT.value,
|
||||
"label": {"en": "React (Legacy)", "fr": "React (Hérité)"},
|
||||
},
|
||||
]})
|
||||
maxSteps: int = Field(default=10, description="Maximum number of iterations in dynamic mode", json_schema_extra={"frontend_type": "integer", "frontend_readonly": False, "frontend_required": False})
|
||||
expectedFormats: Optional[List[str]] = Field(None, description="List of expected file format extensions from user request (e.g., ['xlsx', 'pdf']). Extracted during intent analysis.", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
||||
|
|
|
|||
579
modules/datamodels/datamodelTrustee.py
Normal file
579
modules/datamodels/datamodelTrustee.py
Normal file
|
|
@ -0,0 +1,579 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# All rights reserved.
|
||||
"""Trustee models: TrusteeOrganisation, TrusteeRole, TrusteeAccess, TrusteeContract, TrusteeDocument, TrusteePosition, TrusteePositionDocument."""
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
from modules.shared.attributeUtils import registerModelLabels
|
||||
import uuid
|
||||
|
||||
|
||||
class TrusteeOrganisation(BaseModel):
|
||||
"""Represents trustee organisations (companies) within the Trustee feature."""
|
||||
id: str = Field( # Unique string label (PK), not UUID
|
||||
description="Unique organisation identifier (label)",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": False, # Editable at creation, then readonly
|
||||
"frontend_required": True
|
||||
}
|
||||
)
|
||||
label: str = Field(
|
||||
description="Company name",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True
|
||||
}
|
||||
)
|
||||
enabled: bool = Field(
|
||||
default=True,
|
||||
description="Whether the organisation is enabled",
|
||||
json_schema_extra={
|
||||
"frontend_type": "checkbox",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
mandateId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Mandate ID (system-level organisation)",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
# System attributes are automatically set by DatabaseConnector:
|
||||
# _createdAt, _modifiedAt, _createdBy, _modifiedBy
|
||||
|
||||
|
||||
registerModelLabels(
|
||||
"TrusteeOrganisation",
|
||||
{"en": "Organisation", "fr": "Organisation", "de": "Organisation"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
||||
"label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
|
||||
"enabled": {"en": "Enabled", "fr": "Activé", "de": "Aktiviert"},
|
||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class TrusteeRole(BaseModel):
|
||||
"""Defines roles within the Trustee feature."""
|
||||
id: str = Field( # Unique string label (PK), not UUID
|
||||
description="Unique role identifier (label)",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True
|
||||
}
|
||||
)
|
||||
desc: str = Field(
|
||||
description="Role description",
|
||||
json_schema_extra={
|
||||
"frontend_type": "textarea",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True
|
||||
}
|
||||
)
|
||||
mandateId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Mandate ID",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
# System attributes are automatically set by DatabaseConnector
|
||||
|
||||
|
||||
registerModelLabels(
|
||||
"TrusteeRole",
|
||||
{"en": "Role", "fr": "Rôle", "de": "Rolle"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
||||
"desc": {"en": "Description", "fr": "Description", "de": "Beschreibung"},
|
||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class TrusteeAccess(BaseModel):
|
||||
"""Defines user access to organisations with specific roles."""
|
||||
id: str = Field(
|
||||
default_factory=lambda: str(uuid.uuid4()),
|
||||
description="Unique access ID",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
organisationId: str = Field(
|
||||
description="Reference to TrusteeOrganisation.id",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": "TrusteeOrganisation"
|
||||
}
|
||||
)
|
||||
roleId: str = Field(
|
||||
description="Reference to TrusteeRole.id",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": "TrusteeRole"
|
||||
}
|
||||
)
|
||||
userId: str = Field(
|
||||
description="User ID assigned to this role",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": "User"
|
||||
}
|
||||
)
|
||||
contractId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Optional reference to TrusteeContract.id. If None, access is for full organisation. If set, access is limited to this specific contract.",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": False,
|
||||
"frontend_options": "TrusteeContract",
|
||||
"frontend_depends_on": "organisationId"
|
||||
}
|
||||
)
|
||||
mandateId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Mandate ID",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
# System attributes are automatically set by DatabaseConnector
|
||||
|
||||
|
||||
registerModelLabels(
|
||||
"TrusteeAccess",
|
||||
{"en": "Access", "fr": "Accès", "de": "Zugriff"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
||||
"organisationId": {"en": "Organisation", "fr": "Organisation", "de": "Organisation"},
|
||||
"roleId": {"en": "Role", "fr": "Rôle", "de": "Rolle"},
|
||||
"userId": {"en": "User", "fr": "Utilisateur", "de": "Benutzer"},
|
||||
"contractId": {"en": "Contract (optional)", "fr": "Contrat (optionnel)", "de": "Vertrag (optional)"},
|
||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class TrusteeContract(BaseModel):
|
||||
"""Defines customer contracts within organisations."""
|
||||
id: str = Field(
|
||||
default_factory=lambda: str(uuid.uuid4()),
|
||||
description="Unique contract ID",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
organisationId: str = Field(
|
||||
description="Reference to TrusteeOrganisation.id (immutable after creation)",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False, # Editable at creation, then readonly
|
||||
"frontend_required": True,
|
||||
"frontend_options": "TrusteeOrganisation"
|
||||
}
|
||||
)
|
||||
label: str = Field(
|
||||
description="Label for the customer contract (e.g., 'Muster AG 2026')",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True
|
||||
}
|
||||
)
|
||||
enabled: bool = Field(
|
||||
default=True,
|
||||
description="Whether the contract is enabled",
|
||||
json_schema_extra={
|
||||
"frontend_type": "checkbox",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
mandateId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Mandate ID",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
# System attributes are automatically set by DatabaseConnector
|
||||
|
||||
|
||||
registerModelLabels(
|
||||
"TrusteeContract",
|
||||
{"en": "Contract", "fr": "Contrat", "de": "Vertrag"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
||||
"organisationId": {"en": "Organisation", "fr": "Organisation", "de": "Organisation"},
|
||||
"label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
|
||||
"enabled": {"en": "Enabled", "fr": "Activé", "de": "Aktiviert"},
|
||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class TrusteeDocument(BaseModel):
|
||||
"""Contains document references and receipts for bookings."""
|
||||
id: str = Field(
|
||||
default_factory=lambda: str(uuid.uuid4()),
|
||||
description="Unique document ID",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
organisationId: str = Field(
|
||||
description="Reference to TrusteeOrganisation.id",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": "TrusteeOrganisation"
|
||||
}
|
||||
)
|
||||
contractId: str = Field(
|
||||
description="Reference to TrusteeContract.id",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": "TrusteeContract",
|
||||
"frontend_depends_on": "organisationId"
|
||||
}
|
||||
)
|
||||
documentData: Optional[bytes] = Field(
|
||||
default=None,
|
||||
description="The file content (binary)",
|
||||
json_schema_extra={
|
||||
"frontend_type": "file",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
documentName: str = Field(
|
||||
description="File name (e.g., 'Beleg.pdf')",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True
|
||||
}
|
||||
)
|
||||
documentMimeType: str = Field(
|
||||
default="application/octet-stream",
|
||||
description="MIME type of the document",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": [
|
||||
{"value": "application/pdf", "label": {"en": "PDF", "fr": "PDF", "de": "PDF"}},
|
||||
{"value": "image/jpeg", "label": {"en": "JPEG", "fr": "JPEG", "de": "JPEG"}},
|
||||
{"value": "image/png", "label": {"en": "PNG", "fr": "PNG", "de": "PNG"}},
|
||||
{"value": "application/octet-stream", "label": {"en": "Other", "fr": "Autre", "de": "Andere"}},
|
||||
]
|
||||
}
|
||||
)
|
||||
mandateId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Mandate ID",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
# System attributes are automatically set by DatabaseConnector
|
||||
|
||||
|
||||
registerModelLabels(
|
||||
"TrusteeDocument",
|
||||
{"en": "Document", "fr": "Document", "de": "Dokument"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
||||
"organisationId": {"en": "Organisation", "fr": "Organisation", "de": "Organisation"},
|
||||
"contractId": {"en": "Contract", "fr": "Contrat", "de": "Vertrag"},
|
||||
"documentData": {"en": "Document Data", "fr": "Données du document", "de": "Dokumentdaten"},
|
||||
"documentName": {"en": "Document Name", "fr": "Nom du document", "de": "Dokumentname"},
|
||||
"documentMimeType": {"en": "MIME Type", "fr": "Type MIME", "de": "MIME-Typ"},
|
||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class TrusteePosition(BaseModel):
|
||||
"""Contains booking positions (expense entries)."""
|
||||
id: str = Field(
|
||||
default_factory=lambda: str(uuid.uuid4()),
|
||||
description="Unique position ID",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
organisationId: str = Field(
|
||||
description="Reference to TrusteeOrganisation.id",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": "TrusteeOrganisation"
|
||||
}
|
||||
)
|
||||
contractId: str = Field(
|
||||
description="Reference to TrusteeContract.id",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": "TrusteeContract",
|
||||
"frontend_depends_on": "organisationId"
|
||||
}
|
||||
)
|
||||
valuta: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Value date (ISO format: YYYY-MM-DD)",
|
||||
json_schema_extra={
|
||||
"frontend_type": "date",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True
|
||||
}
|
||||
)
|
||||
transactionDateTime: Optional[float] = Field(
|
||||
default=None,
|
||||
description="Transaction timestamp (UTC timestamp in seconds)",
|
||||
json_schema_extra={
|
||||
"frontend_type": "timestamp",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True
|
||||
}
|
||||
)
|
||||
company: str = Field(
|
||||
default="",
|
||||
description="Company name",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
desc: str = Field(
|
||||
default="",
|
||||
description="Description",
|
||||
json_schema_extra={
|
||||
"frontend_type": "textarea",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
tags: str = Field(
|
||||
default="",
|
||||
description="Tags (comma-separated)",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
bookingCurrency: str = Field(
|
||||
default="CHF",
|
||||
description="Booking currency code",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": [
|
||||
{"value": "CHF", "label": {"en": "CHF", "fr": "CHF", "de": "CHF"}},
|
||||
{"value": "EUR", "label": {"en": "EUR", "fr": "EUR", "de": "EUR"}},
|
||||
{"value": "USD", "label": {"en": "USD", "fr": "USD", "de": "USD"}},
|
||||
{"value": "GBP", "label": {"en": "GBP", "fr": "GBP", "de": "GBP"}},
|
||||
]
|
||||
}
|
||||
)
|
||||
bookingAmount: float = Field(
|
||||
default=0.0,
|
||||
description="Booking amount",
|
||||
json_schema_extra={
|
||||
"frontend_type": "number",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True
|
||||
}
|
||||
)
|
||||
originalCurrency: str = Field(
|
||||
default="CHF",
|
||||
description="Original currency code",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": [
|
||||
{"value": "CHF", "label": {"en": "CHF", "fr": "CHF", "de": "CHF"}},
|
||||
{"value": "EUR", "label": {"en": "EUR", "fr": "EUR", "de": "EUR"}},
|
||||
{"value": "USD", "label": {"en": "USD", "fr": "USD", "de": "USD"}},
|
||||
{"value": "GBP", "label": {"en": "GBP", "fr": "GBP", "de": "GBP"}},
|
||||
]
|
||||
}
|
||||
)
|
||||
originalAmount: float = Field(
|
||||
default=0.0,
|
||||
description="Original amount (manual input, no automatic currency conversion)",
|
||||
json_schema_extra={
|
||||
"frontend_type": "number",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True
|
||||
}
|
||||
)
|
||||
vatPercentage: float = Field(
|
||||
default=0.0,
|
||||
description="VAT percentage",
|
||||
json_schema_extra={
|
||||
"frontend_type": "number",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
vatAmount: float = Field(
|
||||
default=0.0,
|
||||
description="VAT amount (calculated: bookingAmount * vatPercentage / 100, can be manually overridden)",
|
||||
json_schema_extra={
|
||||
"frontend_type": "number",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
mandateId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Mandate ID",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
# System attributes are automatically set by DatabaseConnector
|
||||
|
||||
|
||||
registerModelLabels(
|
||||
"TrusteePosition",
|
||||
{"en": "Position", "fr": "Position", "de": "Position"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
||||
"organisationId": {"en": "Organisation", "fr": "Organisation", "de": "Organisation"},
|
||||
"contractId": {"en": "Contract", "fr": "Contrat", "de": "Vertrag"},
|
||||
"valuta": {"en": "Value Date", "fr": "Date de valeur", "de": "Valutadatum"},
|
||||
"transactionDateTime": {"en": "Transaction Date/Time", "fr": "Date/Heure de transaction", "de": "Transaktionszeitpunkt"},
|
||||
"company": {"en": "Company", "fr": "Entreprise", "de": "Firma"},
|
||||
"desc": {"en": "Description", "fr": "Description", "de": "Beschreibung"},
|
||||
"tags": {"en": "Tags", "fr": "Tags", "de": "Tags"},
|
||||
"bookingCurrency": {"en": "Booking Currency", "fr": "Devise de comptabilisation", "de": "Buchungswährung"},
|
||||
"bookingAmount": {"en": "Booking Amount", "fr": "Montant de comptabilisation", "de": "Buchungsbetrag"},
|
||||
"originalCurrency": {"en": "Original Currency", "fr": "Devise d'origine", "de": "Originalwährung"},
|
||||
"originalAmount": {"en": "Original Amount", "fr": "Montant d'origine", "de": "Originalbetrag"},
|
||||
"vatPercentage": {"en": "VAT Percentage", "fr": "Pourcentage TVA", "de": "MwSt-Prozentsatz"},
|
||||
"vatAmount": {"en": "VAT Amount", "fr": "Montant TVA", "de": "MwSt-Betrag"},
|
||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class TrusteePositionDocument(BaseModel):
|
||||
"""Cross-reference table linking positions to documents (many-to-many)."""
|
||||
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
|
||||
}
|
||||
)
|
||||
organisationId: str = Field(
|
||||
description="Reference to TrusteeOrganisation.id",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": "TrusteeOrganisation"
|
||||
}
|
||||
)
|
||||
contractId: str = Field(
|
||||
description="Reference to TrusteeContract.id",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": "TrusteeContract",
|
||||
"frontend_depends_on": "organisationId"
|
||||
}
|
||||
)
|
||||
documentId: str = Field(
|
||||
description="Reference to TrusteeDocument.id",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": "TrusteeDocument",
|
||||
"frontend_depends_on": "contractId"
|
||||
}
|
||||
)
|
||||
positionId: str = Field(
|
||||
description="Reference to TrusteePosition.id",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": "TrusteePosition",
|
||||
"frontend_depends_on": "contractId"
|
||||
}
|
||||
)
|
||||
mandateId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Mandate ID",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
# 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"},
|
||||
"organisationId": {"en": "Organisation", "fr": "Organisation", "de": "Organisation"},
|
||||
"contractId": {"en": "Contract", "fr": "Contrat", "de": "Vertrag"},
|
||||
"documentId": {"en": "Document", "fr": "Document", "de": "Dokument"},
|
||||
"positionId": {"en": "Position", "fr": "Position", "de": "Position"},
|
||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
||||
},
|
||||
)
|
||||
|
|
@ -10,7 +10,7 @@ import uuid
|
|||
|
||||
class Prompt(BaseModel):
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
||||
mandateId: str = Field(description="ID of the mandate this prompt belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
||||
mandateId: str = Field(default="", description="ID of the mandate this prompt belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
||||
content: str = Field(description="Content of the prompt", json_schema_extra={"frontend_type": "textarea", "frontend_readonly": False, "frontend_required": True})
|
||||
name: str = Field(description="Name of the prompt", json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True})
|
||||
registerModelLabels(
|
||||
|
|
|
|||
|
|
@ -951,7 +951,12 @@ def _addMissingTableRules(db: DatabaseConnector, existingRules: List[Dict[str, A
|
|||
existingRoles = {rule.get("roleLabel") for rule in existingRules}
|
||||
|
||||
# Tables that need rules
|
||||
requiredTables = ["ChatWorkflow", "Prompt", "Projekt", "Parzelle", "Dokument", "Gemeinde", "Kanton", "Land"]
|
||||
requiredTables = [
|
||||
"ChatWorkflow", "Prompt", "Projekt", "Parzelle", "Dokument", "Gemeinde", "Kanton", "Land",
|
||||
# Trustee tables
|
||||
"TrusteeOrganisation", "TrusteeRole", "TrusteeAccess", "TrusteeContract",
|
||||
"TrusteeDocument", "TrusteePosition", "TrusteePositionDocument"
|
||||
]
|
||||
requiredRoles = ["sysadmin", "admin", "user", "viewer"]
|
||||
|
||||
newRules = []
|
||||
|
|
@ -1100,6 +1105,55 @@ def _addMissingTableRules(db: DatabaseConnector, existingRules: List[Dict[str, A
|
|||
update=AccessLevel.NONE,
|
||||
delete=AccessLevel.NONE,
|
||||
))
|
||||
# Trustee tables rules
|
||||
elif table in ["TrusteeOrganisation", "TrusteeRole", "TrusteeAccess", "TrusteeContract",
|
||||
"TrusteeDocument", "TrusteePosition", "TrusteePositionDocument"]:
|
||||
for roleLabel in requiredRoles:
|
||||
if roleLabel == "sysadmin":
|
||||
newRules.append(AccessRule(
|
||||
roleLabel=roleLabel,
|
||||
context=AccessRuleContext.DATA,
|
||||
item=table,
|
||||
view=True,
|
||||
read=AccessLevel.ALL,
|
||||
create=AccessLevel.ALL,
|
||||
update=AccessLevel.ALL,
|
||||
delete=AccessLevel.ALL,
|
||||
))
|
||||
elif roleLabel == "admin":
|
||||
newRules.append(AccessRule(
|
||||
roleLabel=roleLabel,
|
||||
context=AccessRuleContext.DATA,
|
||||
item=table,
|
||||
view=True,
|
||||
read=AccessLevel.GROUP,
|
||||
create=AccessLevel.GROUP,
|
||||
update=AccessLevel.GROUP,
|
||||
delete=AccessLevel.GROUP,
|
||||
))
|
||||
elif roleLabel == "user":
|
||||
# User role: Access to MY records (feature-specific access via trustee.access)
|
||||
newRules.append(AccessRule(
|
||||
roleLabel=roleLabel,
|
||||
context=AccessRuleContext.DATA,
|
||||
item=table,
|
||||
view=True,
|
||||
read=AccessLevel.MY,
|
||||
create=AccessLevel.MY,
|
||||
update=AccessLevel.MY,
|
||||
delete=AccessLevel.MY,
|
||||
))
|
||||
elif roleLabel == "viewer":
|
||||
newRules.append(AccessRule(
|
||||
roleLabel=roleLabel,
|
||||
context=AccessRuleContext.DATA,
|
||||
item=table,
|
||||
view=True,
|
||||
read=AccessLevel.MY,
|
||||
create=AccessLevel.NONE,
|
||||
update=AccessLevel.NONE,
|
||||
delete=AccessLevel.NONE,
|
||||
))
|
||||
|
||||
# Create missing rules
|
||||
if newRules:
|
||||
|
|
|
|||
|
|
@ -272,8 +272,12 @@ class AppObjects:
|
|||
record_value = record.get(field_name)
|
||||
|
||||
# Handle simple value (equals operator)
|
||||
# Compare as strings to handle type mismatches (frontend sends strings)
|
||||
if not isinstance(filter_value, dict):
|
||||
if record_value != filter_value:
|
||||
# Convert both to strings for comparison (case-insensitive for strings)
|
||||
recordStr = str(record_value).lower() if record_value is not None else ""
|
||||
filterStr = str(filter_value).lower() if filter_value is not None else ""
|
||||
if recordStr != filterStr:
|
||||
matches = False
|
||||
break
|
||||
continue
|
||||
|
|
@ -283,7 +287,10 @@ class AppObjects:
|
|||
filter_val = filter_value.get("value")
|
||||
|
||||
if operator in ["equals", "eq"]:
|
||||
if record_value != filter_val:
|
||||
# Convert both to strings for comparison
|
||||
recordStr = str(record_value).lower() if record_value is not None else ""
|
||||
filterStr = str(filter_val).lower() if filter_val is not None else ""
|
||||
if recordStr != filterStr:
|
||||
matches = False
|
||||
break
|
||||
|
||||
|
|
@ -1866,7 +1873,8 @@ class AppObjects:
|
|||
Updated AccessRule object
|
||||
"""
|
||||
try:
|
||||
updatedRule = self.db.recordModify(AccessRule, ruleId, accessRule.model_dump())
|
||||
# Exclude id from model_dump - the URL ruleId is authoritative
|
||||
updatedRule = self.db.recordModify(AccessRule, ruleId, accessRule.model_dump(exclude={"id"}))
|
||||
logger.info(f"Updated access rule with ID {ruleId}")
|
||||
return AccessRule(**updatedRule)
|
||||
except Exception as e:
|
||||
|
|
@ -2149,7 +2157,8 @@ class AppObjects:
|
|||
if conflictingRole and conflictingRole.id != roleId:
|
||||
raise ValueError(f"Role with label '{role.roleLabel}' already exists")
|
||||
|
||||
updatedRole = self.db.recordModify(Role, roleId, role.model_dump())
|
||||
# Exclude id from model_dump - the URL roleId is authoritative
|
||||
updatedRole = self.db.recordModify(Role, roleId, role.model_dump(exclude={"id"}))
|
||||
logger.info(f"Updated role with ID {roleId}")
|
||||
return Role(**updatedRole)
|
||||
except Exception as e:
|
||||
|
|
@ -2177,7 +2186,7 @@ class AppObjects:
|
|||
raise ValueError(f"Cannot delete system role '{role.roleLabel}'")
|
||||
|
||||
# Check if role is assigned to any users
|
||||
allUsers = self.getUsers()
|
||||
allUsers = self.getUsersByMandate(None) # Get all users across all mandates
|
||||
for user in allUsers:
|
||||
if role.roleLabel in (user.roleLabels or []):
|
||||
raise ValueError(f"Cannot delete role '{role.roleLabel}' - it is assigned to users")
|
||||
|
|
|
|||
836
modules/interfaces/interfaceDbTrusteeObjects.py
Normal file
836
modules/interfaces/interfaceDbTrusteeObjects.py
Normal file
|
|
@ -0,0 +1,836 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# All rights reserved.
|
||||
"""
|
||||
Interface to Trustee database.
|
||||
Manages trustee organisations, roles, access, contracts, documents, and positions.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import math
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
from modules.connectors.connectorDbPostgre import DatabaseConnector
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
from modules.interfaces.interfaceRbac import getRecordsetWithRBAC
|
||||
from modules.security.rbac import RbacClass
|
||||
from modules.datamodels.datamodelUam import User, AccessLevel
|
||||
from modules.datamodels.datamodelRbac import AccessRuleContext
|
||||
from modules.datamodels.datamodelTrustee import (
|
||||
TrusteeOrganisation,
|
||||
TrusteeRole,
|
||||
TrusteeAccess,
|
||||
TrusteeContract,
|
||||
TrusteeDocument,
|
||||
TrusteePosition,
|
||||
TrusteePositionDocument,
|
||||
)
|
||||
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Singleton factory for TrusteeObjects instances per context
|
||||
_trusteeInterfaces = {}
|
||||
|
||||
|
||||
def getInterface(currentUser: User) -> "TrusteeObjects":
|
||||
"""Get or create a TrusteeObjects instance for the given user context."""
|
||||
global _trusteeInterfaces
|
||||
|
||||
if not currentUser or not currentUser.id:
|
||||
raise ValueError("Valid user context required")
|
||||
|
||||
cacheKey = f"{currentUser.id}_{currentUser.mandateId}"
|
||||
|
||||
if cacheKey not in _trusteeInterfaces:
|
||||
_trusteeInterfaces[cacheKey] = TrusteeObjects(currentUser)
|
||||
else:
|
||||
# Update user context if needed
|
||||
_trusteeInterfaces[cacheKey].setUserContext(currentUser)
|
||||
|
||||
return _trusteeInterfaces[cacheKey]
|
||||
|
||||
|
||||
class TrusteeObjects:
|
||||
"""
|
||||
Interface to Trustee database.
|
||||
Manages trustee organisations, roles, access, contracts, documents, and positions.
|
||||
"""
|
||||
|
||||
def __init__(self, currentUser: Optional[User] = None):
|
||||
"""Initializes the Trustee Interface."""
|
||||
self.currentUser = currentUser
|
||||
self.userId = currentUser.id if currentUser else None
|
||||
self.mandateId = currentUser.mandateId if currentUser else None
|
||||
self.rbac = None
|
||||
|
||||
# Initialize database
|
||||
self._initializeDatabase()
|
||||
|
||||
# Set user context if provided
|
||||
if currentUser:
|
||||
self.setUserContext(currentUser)
|
||||
|
||||
def setUserContext(self, currentUser: User):
|
||||
"""Sets the user context for the interface."""
|
||||
if not currentUser:
|
||||
logger.info("Initializing interface without user context")
|
||||
return
|
||||
|
||||
self.currentUser = currentUser
|
||||
self.userId = currentUser.id
|
||||
self.mandateId = currentUser.mandateId
|
||||
|
||||
if not self.userId or not self.mandateId:
|
||||
raise ValueError("Invalid user context: id and mandateId are required")
|
||||
|
||||
self.userLanguage = currentUser.language
|
||||
|
||||
# Initialize RBAC interface
|
||||
from modules.security.rootAccess import getRootDbAppConnector
|
||||
dbApp = getRootDbAppConnector()
|
||||
self.rbac = RbacClass(self.db, dbApp=dbApp)
|
||||
|
||||
# Update database context
|
||||
self.db.updateContext(self.userId)
|
||||
|
||||
def __del__(self):
|
||||
"""Cleanup method to close database connection."""
|
||||
if hasattr(self, "db") and self.db is not None:
|
||||
try:
|
||||
self.db.close()
|
||||
except Exception as e:
|
||||
logger.error(f"Error closing database connection: {e}")
|
||||
|
||||
def _initializeDatabase(self):
|
||||
"""Initializes the database connection directly."""
|
||||
try:
|
||||
dbHost = APP_CONFIG.get("DB_TRUSTEE_HOST", APP_CONFIG.get("DB_CHAT_HOST", "_no_config_default_data"))
|
||||
dbDatabase = APP_CONFIG.get("DB_TRUSTEE_DATABASE", "trustee")
|
||||
dbUser = APP_CONFIG.get("DB_TRUSTEE_USER", APP_CONFIG.get("DB_CHAT_USER"))
|
||||
dbPassword = APP_CONFIG.get("DB_TRUSTEE_PASSWORD_SECRET", APP_CONFIG.get("DB_CHAT_PASSWORD_SECRET"))
|
||||
dbPort = int(APP_CONFIG.get("DB_TRUSTEE_PORT", APP_CONFIG.get("DB_CHAT_PORT", 5432)))
|
||||
|
||||
self.db = DatabaseConnector(
|
||||
dbHost=dbHost,
|
||||
dbDatabase=dbDatabase,
|
||||
dbUser=dbUser,
|
||||
dbPassword=dbPassword,
|
||||
dbPort=dbPort,
|
||||
userId=self.userId,
|
||||
)
|
||||
|
||||
self.db.initDbSystem()
|
||||
logger.info(f"Trustee database initialized successfully for user {self.userId}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize Trustee database: {str(e)}")
|
||||
raise
|
||||
|
||||
def checkRbacPermission(
|
||||
self,
|
||||
modelClass: type,
|
||||
operation: str,
|
||||
recordId: Optional[str] = None
|
||||
) -> bool:
|
||||
"""Check RBAC permission for a specific operation on a table."""
|
||||
if not self.rbac or not self.currentUser:
|
||||
return False
|
||||
|
||||
tableName = modelClass.__name__
|
||||
permissions = self.rbac.getUserPermissions(
|
||||
self.currentUser,
|
||||
AccessRuleContext.DATA,
|
||||
tableName
|
||||
)
|
||||
|
||||
if not permissions.view:
|
||||
return False
|
||||
|
||||
permLevel = getattr(permissions, operation, AccessLevel.NONE)
|
||||
if permLevel == AccessLevel.NONE:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# ===== Organisation CRUD =====
|
||||
|
||||
def createOrganisation(self, data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Create a new organisation."""
|
||||
if not self.checkRbacPermission(TrusteeOrganisation, "create"):
|
||||
logger.warning(f"User {self.userId} lacks permission to create organisation")
|
||||
return None
|
||||
|
||||
# Set mandateId from current user
|
||||
data["mandateId"] = self.mandateId
|
||||
|
||||
# Validate ID format (alphanumeric, hyphens, underscores, 3-50 chars)
|
||||
orgId = data.get("id", "")
|
||||
if not orgId or len(orgId) < 3 or len(orgId) > 50:
|
||||
logger.error(f"Invalid organisation ID length: {len(orgId)}")
|
||||
return None
|
||||
|
||||
import re
|
||||
if not re.match(r'^[a-zA-Z0-9_-]+$', orgId):
|
||||
logger.error(f"Invalid organisation ID format: {orgId}")
|
||||
return None
|
||||
|
||||
success = self.db.saveRecord(TrusteeOrganisation, orgId, data)
|
||||
if success:
|
||||
return self.db.getRecord(TrusteeOrganisation, orgId)
|
||||
return None
|
||||
|
||||
def getOrganisation(self, orgId: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get a single organisation by ID."""
|
||||
return self.db.getRecord(TrusteeOrganisation, orgId)
|
||||
|
||||
def getAllOrganisations(self, params: Optional[PaginationParams] = None) -> PaginatedResult:
|
||||
"""Get all organisations with RBAC filtering."""
|
||||
records = getRecordsetWithRBAC(
|
||||
connector=self.db,
|
||||
modelClass=TrusteeOrganisation,
|
||||
currentUser=self.currentUser,
|
||||
recordFilter=None,
|
||||
orderBy="id"
|
||||
)
|
||||
|
||||
# Apply pagination
|
||||
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 updateOrganisation(self, orgId: str, data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Update an organisation."""
|
||||
if not self.checkRbacPermission(TrusteeOrganisation, "update"):
|
||||
logger.warning(f"User {self.userId} lacks permission to update organisation")
|
||||
return None
|
||||
|
||||
# ID cannot be changed after creation
|
||||
if "id" in data and data["id"] != orgId:
|
||||
logger.error("Organisation ID cannot be changed")
|
||||
return None
|
||||
|
||||
data["id"] = orgId
|
||||
success = self.db.saveRecord(TrusteeOrganisation, orgId, data)
|
||||
if success:
|
||||
return self.db.getRecord(TrusteeOrganisation, orgId)
|
||||
return None
|
||||
|
||||
def deleteOrganisation(self, orgId: str) -> bool:
|
||||
"""Delete an organisation."""
|
||||
if not self.checkRbacPermission(TrusteeOrganisation, "delete"):
|
||||
logger.warning(f"User {self.userId} lacks permission to delete organisation")
|
||||
return False
|
||||
|
||||
return self.db.deleteRecord(TrusteeOrganisation, orgId)
|
||||
|
||||
# ===== Role CRUD =====
|
||||
|
||||
def createRole(self, data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Create a new role (sysadmin only)."""
|
||||
if not self.checkRbacPermission(TrusteeRole, "create"):
|
||||
logger.warning(f"User {self.userId} lacks permission to create role")
|
||||
return None
|
||||
|
||||
data["mandateId"] = self.mandateId
|
||||
roleId = data.get("id", "")
|
||||
|
||||
if not roleId:
|
||||
logger.error("Role ID is required")
|
||||
return None
|
||||
|
||||
success = self.db.saveRecord(TrusteeRole, roleId, data)
|
||||
if success:
|
||||
return self.db.getRecord(TrusteeRole, roleId)
|
||||
return None
|
||||
|
||||
def getRole(self, roleId: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get a single role by ID."""
|
||||
return self.db.getRecord(TrusteeRole, roleId)
|
||||
|
||||
def getAllRoles(self, params: Optional[PaginationParams] = None) -> PaginatedResult:
|
||||
"""Get all roles with RBAC filtering."""
|
||||
records = getRecordsetWithRBAC(
|
||||
connector=self.db,
|
||||
modelClass=TrusteeRole,
|
||||
currentUser=self.currentUser,
|
||||
recordFilter=None,
|
||||
orderBy="id"
|
||||
)
|
||||
|
||||
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 updateRole(self, roleId: str, data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Update a role (sysadmin only)."""
|
||||
if not self.checkRbacPermission(TrusteeRole, "update"):
|
||||
logger.warning(f"User {self.userId} lacks permission to update role")
|
||||
return None
|
||||
|
||||
data["id"] = roleId
|
||||
success = self.db.saveRecord(TrusteeRole, roleId, data)
|
||||
if success:
|
||||
return self.db.getRecord(TrusteeRole, roleId)
|
||||
return None
|
||||
|
||||
def deleteRole(self, roleId: str) -> bool:
|
||||
"""Delete a role (sysadmin only, not if in use)."""
|
||||
if not self.checkRbacPermission(TrusteeRole, "delete"):
|
||||
logger.warning(f"User {self.userId} lacks permission to delete role")
|
||||
return False
|
||||
|
||||
# Check if role is in use
|
||||
accessRecords = self.db.getRecordset(TrusteeAccess, {"roleId": roleId})
|
||||
if accessRecords:
|
||||
logger.error(f"Cannot delete role {roleId}: still in use")
|
||||
return False
|
||||
|
||||
return self.db.deleteRecord(TrusteeRole, roleId)
|
||||
|
||||
# ===== Access CRUD =====
|
||||
|
||||
def createAccess(self, data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Create a new access record."""
|
||||
if not self.checkRbacPermission(TrusteeAccess, "create"):
|
||||
logger.warning(f"User {self.userId} lacks permission to create access")
|
||||
return None
|
||||
|
||||
data["mandateId"] = self.mandateId
|
||||
|
||||
import uuid
|
||||
accessId = data.get("id") or str(uuid.uuid4())
|
||||
data["id"] = accessId
|
||||
|
||||
success = self.db.saveRecord(TrusteeAccess, accessId, data)
|
||||
if success:
|
||||
return self.db.getRecord(TrusteeAccess, accessId)
|
||||
return None
|
||||
|
||||
def getAccess(self, accessId: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get a single access record by ID."""
|
||||
return self.db.getRecord(TrusteeAccess, accessId)
|
||||
|
||||
def getAllAccess(self, params: Optional[PaginationParams] = None) -> PaginatedResult:
|
||||
"""Get all access records with RBAC filtering."""
|
||||
records = getRecordsetWithRBAC(
|
||||
connector=self.db,
|
||||
modelClass=TrusteeAccess,
|
||||
currentUser=self.currentUser,
|
||||
recordFilter=None,
|
||||
orderBy="id"
|
||||
)
|
||||
|
||||
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 getAccessByOrganisation(self, organisationId: str) -> List[Dict[str, Any]]:
|
||||
"""Get all access records for a specific organisation."""
|
||||
return getRecordsetWithRBAC(
|
||||
connector=self.db,
|
||||
modelClass=TrusteeAccess,
|
||||
currentUser=self.currentUser,
|
||||
recordFilter={"organisationId": organisationId},
|
||||
orderBy="id"
|
||||
)
|
||||
|
||||
def getAccessByUser(self, userId: str) -> List[Dict[str, Any]]:
|
||||
"""Get all access records for a specific user."""
|
||||
return getRecordsetWithRBAC(
|
||||
connector=self.db,
|
||||
modelClass=TrusteeAccess,
|
||||
currentUser=self.currentUser,
|
||||
recordFilter={"userId": userId},
|
||||
orderBy="id"
|
||||
)
|
||||
|
||||
def updateAccess(self, accessId: str, data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Update an access record."""
|
||||
if not self.checkRbacPermission(TrusteeAccess, "update"):
|
||||
logger.warning(f"User {self.userId} lacks permission to update access")
|
||||
return None
|
||||
|
||||
data["id"] = accessId
|
||||
success = self.db.saveRecord(TrusteeAccess, accessId, data)
|
||||
if success:
|
||||
return self.db.getRecord(TrusteeAccess, accessId)
|
||||
return None
|
||||
|
||||
def deleteAccess(self, accessId: str) -> bool:
|
||||
"""Delete an access record."""
|
||||
if not self.checkRbacPermission(TrusteeAccess, "delete"):
|
||||
logger.warning(f"User {self.userId} lacks permission to delete access")
|
||||
return False
|
||||
|
||||
return self.db.deleteRecord(TrusteeAccess, accessId)
|
||||
|
||||
# ===== Contract CRUD =====
|
||||
|
||||
def createContract(self, data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Create a new contract."""
|
||||
if not self.checkRbacPermission(TrusteeContract, "create"):
|
||||
logger.warning(f"User {self.userId} lacks permission to create contract")
|
||||
return None
|
||||
|
||||
data["mandateId"] = self.mandateId
|
||||
|
||||
import uuid
|
||||
contractId = data.get("id") or str(uuid.uuid4())
|
||||
data["id"] = contractId
|
||||
|
||||
success = self.db.saveRecord(TrusteeContract, contractId, data)
|
||||
if success:
|
||||
return self.db.getRecord(TrusteeContract, contractId)
|
||||
return None
|
||||
|
||||
def getContract(self, contractId: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get a single contract by ID."""
|
||||
return self.db.getRecord(TrusteeContract, contractId)
|
||||
|
||||
def getAllContracts(self, params: Optional[PaginationParams] = None) -> PaginatedResult:
|
||||
"""Get all contracts with RBAC filtering."""
|
||||
records = getRecordsetWithRBAC(
|
||||
connector=self.db,
|
||||
modelClass=TrusteeContract,
|
||||
currentUser=self.currentUser,
|
||||
recordFilter=None,
|
||||
orderBy="id"
|
||||
)
|
||||
|
||||
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 getContractsByOrganisation(self, organisationId: str) -> List[Dict[str, Any]]:
|
||||
"""Get all contracts for a specific organisation."""
|
||||
return getRecordsetWithRBAC(
|
||||
connector=self.db,
|
||||
modelClass=TrusteeContract,
|
||||
currentUser=self.currentUser,
|
||||
recordFilter={"organisationId": organisationId},
|
||||
orderBy="label"
|
||||
)
|
||||
|
||||
def updateContract(self, contractId: str, data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Update a contract (organisationId is immutable)."""
|
||||
if not self.checkRbacPermission(TrusteeContract, "update"):
|
||||
logger.warning(f"User {self.userId} lacks permission to update contract")
|
||||
return None
|
||||
|
||||
# Check if organisationId is being changed
|
||||
existing = self.db.getRecord(TrusteeContract, contractId)
|
||||
if existing and "organisationId" in data:
|
||||
if data["organisationId"] != existing.get("organisationId"):
|
||||
logger.error("Contract organisationId cannot be changed after creation")
|
||||
return None
|
||||
|
||||
data["id"] = contractId
|
||||
success = self.db.saveRecord(TrusteeContract, contractId, data)
|
||||
if success:
|
||||
return self.db.getRecord(TrusteeContract, contractId)
|
||||
return None
|
||||
|
||||
def deleteContract(self, contractId: str) -> bool:
|
||||
"""Delete a contract."""
|
||||
if not self.checkRbacPermission(TrusteeContract, "delete"):
|
||||
logger.warning(f"User {self.userId} lacks permission to delete contract")
|
||||
return False
|
||||
|
||||
return self.db.deleteRecord(TrusteeContract, contractId)
|
||||
|
||||
# ===== Document CRUD =====
|
||||
|
||||
def createDocument(self, data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Create a new document."""
|
||||
if not self.checkRbacPermission(TrusteeDocument, "create"):
|
||||
logger.warning(f"User {self.userId} lacks permission to create document")
|
||||
return None
|
||||
|
||||
data["mandateId"] = self.mandateId
|
||||
|
||||
import uuid
|
||||
documentId = data.get("id") or str(uuid.uuid4())
|
||||
data["id"] = documentId
|
||||
|
||||
success = self.db.saveRecord(TrusteeDocument, documentId, data)
|
||||
if success:
|
||||
return self.db.getRecord(TrusteeDocument, documentId)
|
||||
return None
|
||||
|
||||
def getDocument(self, documentId: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get a single document by ID (metadata only)."""
|
||||
record = self.db.getRecord(TrusteeDocument, documentId)
|
||||
if record:
|
||||
# Remove binary data from response
|
||||
record.pop("documentData", None)
|
||||
return record
|
||||
|
||||
def getDocumentData(self, documentId: str) -> Optional[bytes]:
|
||||
"""Get document binary data."""
|
||||
record = self.db.getRecord(TrusteeDocument, documentId)
|
||||
if record:
|
||||
return record.get("documentData")
|
||||
return None
|
||||
|
||||
def getAllDocuments(self, params: Optional[PaginationParams] = None) -> PaginatedResult:
|
||||
"""Get all documents with RBAC filtering (metadata only)."""
|
||||
records = getRecordsetWithRBAC(
|
||||
connector=self.db,
|
||||
modelClass=TrusteeDocument,
|
||||
currentUser=self.currentUser,
|
||||
recordFilter=None,
|
||||
orderBy="documentName"
|
||||
)
|
||||
|
||||
# Remove binary data from responses
|
||||
for record in records:
|
||||
record.pop("documentData", None)
|
||||
|
||||
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 getDocumentsByContract(self, contractId: str) -> List[Dict[str, Any]]:
|
||||
"""Get all documents for a specific contract."""
|
||||
records = getRecordsetWithRBAC(
|
||||
connector=self.db,
|
||||
modelClass=TrusteeDocument,
|
||||
currentUser=self.currentUser,
|
||||
recordFilter={"contractId": contractId},
|
||||
orderBy="documentName"
|
||||
)
|
||||
for record in records:
|
||||
record.pop("documentData", None)
|
||||
return records
|
||||
|
||||
def updateDocument(self, documentId: str, data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Update a document."""
|
||||
if not self.checkRbacPermission(TrusteeDocument, "update"):
|
||||
logger.warning(f"User {self.userId} lacks permission to update document")
|
||||
return None
|
||||
|
||||
data["id"] = documentId
|
||||
success = self.db.saveRecord(TrusteeDocument, documentId, data)
|
||||
if success:
|
||||
result = self.db.getRecord(TrusteeDocument, documentId)
|
||||
if result:
|
||||
result.pop("documentData", None)
|
||||
return result
|
||||
return None
|
||||
|
||||
def deleteDocument(self, documentId: str) -> bool:
|
||||
"""Delete a document."""
|
||||
if not self.checkRbacPermission(TrusteeDocument, "delete"):
|
||||
logger.warning(f"User {self.userId} lacks permission to delete document")
|
||||
return False
|
||||
|
||||
return self.db.deleteRecord(TrusteeDocument, documentId)
|
||||
|
||||
# ===== Position CRUD =====
|
||||
|
||||
def createPosition(self, data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Create a new position."""
|
||||
if not self.checkRbacPermission(TrusteePosition, "create"):
|
||||
logger.warning(f"User {self.userId} lacks permission to create position")
|
||||
return None
|
||||
|
||||
data["mandateId"] = self.mandateId
|
||||
|
||||
# Calculate VAT amount if not provided
|
||||
if "vatAmount" not in data or data.get("vatAmount") == 0:
|
||||
bookingAmount = data.get("bookingAmount", 0)
|
||||
vatPercentage = data.get("vatPercentage", 0)
|
||||
data["vatAmount"] = bookingAmount * vatPercentage / 100
|
||||
|
||||
import uuid
|
||||
positionId = data.get("id") or str(uuid.uuid4())
|
||||
data["id"] = positionId
|
||||
|
||||
success = self.db.saveRecord(TrusteePosition, positionId, data)
|
||||
if success:
|
||||
return self.db.getRecord(TrusteePosition, positionId)
|
||||
return None
|
||||
|
||||
def getPosition(self, positionId: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get a single position by ID."""
|
||||
return self.db.getRecord(TrusteePosition, positionId)
|
||||
|
||||
def getAllPositions(self, params: Optional[PaginationParams] = None) -> PaginatedResult:
|
||||
"""Get all positions with RBAC filtering."""
|
||||
records = getRecordsetWithRBAC(
|
||||
connector=self.db,
|
||||
modelClass=TrusteePosition,
|
||||
currentUser=self.currentUser,
|
||||
recordFilter=None,
|
||||
orderBy="valuta"
|
||||
)
|
||||
|
||||
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 getPositionsByContract(self, contractId: str) -> List[Dict[str, Any]]:
|
||||
"""Get all positions for a specific contract."""
|
||||
return getRecordsetWithRBAC(
|
||||
connector=self.db,
|
||||
modelClass=TrusteePosition,
|
||||
currentUser=self.currentUser,
|
||||
recordFilter={"contractId": contractId},
|
||||
orderBy="valuta"
|
||||
)
|
||||
|
||||
def getPositionsByOrganisation(self, organisationId: str) -> List[Dict[str, Any]]:
|
||||
"""Get all positions for a specific organisation."""
|
||||
return getRecordsetWithRBAC(
|
||||
connector=self.db,
|
||||
modelClass=TrusteePosition,
|
||||
currentUser=self.currentUser,
|
||||
recordFilter={"organisationId": organisationId},
|
||||
orderBy="valuta"
|
||||
)
|
||||
|
||||
def updatePosition(self, positionId: str, data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Update a position."""
|
||||
if not self.checkRbacPermission(TrusteePosition, "update"):
|
||||
logger.warning(f"User {self.userId} lacks permission to update position")
|
||||
return None
|
||||
|
||||
data["id"] = positionId
|
||||
success = self.db.saveRecord(TrusteePosition, positionId, data)
|
||||
if success:
|
||||
return self.db.getRecord(TrusteePosition, positionId)
|
||||
return None
|
||||
|
||||
def deletePosition(self, positionId: str) -> bool:
|
||||
"""Delete a position."""
|
||||
if not self.checkRbacPermission(TrusteePosition, "delete"):
|
||||
logger.warning(f"User {self.userId} lacks permission to delete position")
|
||||
return False
|
||||
|
||||
return self.db.deleteRecord(TrusteePosition, positionId)
|
||||
|
||||
# ===== Position-Document Link CRUD =====
|
||||
|
||||
def createPositionDocument(self, data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Create a new position-document link."""
|
||||
if not self.checkRbacPermission(TrusteePositionDocument, "create"):
|
||||
logger.warning(f"User {self.userId} lacks permission to create position-document link")
|
||||
return None
|
||||
|
||||
data["mandateId"] = self.mandateId
|
||||
|
||||
import uuid
|
||||
linkId = data.get("id") or str(uuid.uuid4())
|
||||
data["id"] = linkId
|
||||
|
||||
success = self.db.saveRecord(TrusteePositionDocument, linkId, data)
|
||||
if success:
|
||||
return self.db.getRecord(TrusteePositionDocument, linkId)
|
||||
return None
|
||||
|
||||
def getPositionDocument(self, linkId: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get a single position-document link by ID."""
|
||||
return self.db.getRecord(TrusteePositionDocument, linkId)
|
||||
|
||||
def getAllPositionDocuments(self, params: Optional[PaginationParams] = None) -> PaginatedResult:
|
||||
"""Get all position-document links with RBAC filtering."""
|
||||
records = getRecordsetWithRBAC(
|
||||
connector=self.db,
|
||||
modelClass=TrusteePositionDocument,
|
||||
currentUser=self.currentUser,
|
||||
recordFilter=None,
|
||||
orderBy="id"
|
||||
)
|
||||
|
||||
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[Dict[str, Any]]:
|
||||
"""Get all documents linked to a position."""
|
||||
links = getRecordsetWithRBAC(
|
||||
connector=self.db,
|
||||
modelClass=TrusteePositionDocument,
|
||||
currentUser=self.currentUser,
|
||||
recordFilter={"positionId": positionId},
|
||||
orderBy="id"
|
||||
)
|
||||
return links
|
||||
|
||||
def getPositionsForDocument(self, documentId: str) -> List[Dict[str, Any]]:
|
||||
"""Get all positions linked to a document."""
|
||||
links = getRecordsetWithRBAC(
|
||||
connector=self.db,
|
||||
modelClass=TrusteePositionDocument,
|
||||
currentUser=self.currentUser,
|
||||
recordFilter={"documentId": documentId},
|
||||
orderBy="id"
|
||||
)
|
||||
return links
|
||||
|
||||
def deletePositionDocument(self, linkId: str) -> bool:
|
||||
"""Delete a position-document link."""
|
||||
if not self.checkRbacPermission(TrusteePositionDocument, "delete"):
|
||||
logger.warning(f"User {self.userId} lacks permission to delete position-document link")
|
||||
return False
|
||||
|
||||
return self.db.deleteRecord(TrusteePositionDocument, linkId)
|
||||
|
||||
# ===== Trustee-specific Access Check =====
|
||||
|
||||
def getUserAccessForOrganisation(self, userId: str, organisationId: str) -> List[Dict[str, Any]]:
|
||||
"""Get all access records for a user in a specific organisation."""
|
||||
return self.db.getRecordset(
|
||||
TrusteeAccess,
|
||||
{"userId": userId, "organisationId": organisationId}
|
||||
)
|
||||
|
||||
def checkUserTrusteePermission(
|
||||
self,
|
||||
userId: str,
|
||||
organisationId: str,
|
||||
requiredRole: str,
|
||||
contractId: Optional[str] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Check if a user has a specific role for an organisation (and optionally contract).
|
||||
|
||||
Args:
|
||||
userId: User ID to check
|
||||
organisationId: Organisation ID
|
||||
requiredRole: Required role (userreport, admin, operate)
|
||||
contractId: Optional contract ID for contract-specific access
|
||||
|
||||
Returns:
|
||||
True if user has the required role
|
||||
"""
|
||||
accessRecords = self.getUserAccessForOrganisation(userId, organisationId)
|
||||
|
||||
for access in accessRecords:
|
||||
if access.get("roleId") == requiredRole:
|
||||
accessContractId = access.get("contractId")
|
||||
|
||||
# If access has no contractId, it grants access to all contracts
|
||||
if accessContractId is None:
|
||||
return True
|
||||
|
||||
# If checking for specific contract, match it
|
||||
if contractId and accessContractId == contractId:
|
||||
return True
|
||||
|
||||
# If no specific contract requested and access is contract-specific, deny
|
||||
if contractId is None:
|
||||
continue
|
||||
|
||||
return False
|
||||
|
|
@ -401,24 +401,12 @@ async def get_chatbot_threads(
|
|||
# Apply pagination (skip/limit)
|
||||
startIdx = (paginationParams.page - 1) * paginationParams.pageSize
|
||||
endIdx = startIdx + paginationParams.pageSize
|
||||
workflows_data = chatbot_workflows_data[startIdx:endIdx]
|
||||
workflows = chatbot_workflows_data[startIdx:endIdx]
|
||||
else:
|
||||
workflows_data = chatbot_workflows_data
|
||||
workflows = chatbot_workflows_data
|
||||
totalItems = len(chatbot_workflows_data)
|
||||
totalPages = 1
|
||||
|
||||
# Convert raw dictionaries to ChatWorkflow objects
|
||||
workflows = []
|
||||
for workflow_data in workflows_data:
|
||||
try:
|
||||
# Load the workflow properly
|
||||
workflow = interfaceDbChat.getWorkflow(workflow_data["id"])
|
||||
if workflow:
|
||||
workflows.append(workflow)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error loading workflow {workflow_data.get('id')}: {e}")
|
||||
continue
|
||||
|
||||
# Create paginated response
|
||||
from modules.datamodels.datamodelPagination import PaginationMetadata
|
||||
metadata = PaginationMetadata(
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import json
|
|||
from modules.interfaces.interfaceDbChatObjects import getInterface as getChatInterface
|
||||
from modules.auth import getCurrentUser, limiter
|
||||
from modules.datamodels.datamodelChat import AutomationDefinition, ChatWorkflow
|
||||
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata
|
||||
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
|
||||
from modules.shared.attributeUtils import getModelAttributeDefinitions
|
||||
from modules.features.workflow import executeAutomation
|
||||
from modules.features.workflow.subAutomationTemplates import getAutomationTemplates
|
||||
|
|
@ -59,7 +59,9 @@ async def get_automations(
|
|||
if pagination:
|
||||
try:
|
||||
paginationDict = json.loads(pagination)
|
||||
paginationParams = PaginationParams(**paginationDict) if paginationDict else None
|
||||
if paginationDict:
|
||||
paginationDict = normalize_pagination_dict(paginationDict)
|
||||
paginationParams = PaginationParams(**paginationDict)
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from modules.shared.attributeUtils import getModelAttributeDefinitions
|
|||
|
||||
# Import the model classes
|
||||
from modules.datamodels.datamodelUam import Mandate, User
|
||||
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata
|
||||
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -58,7 +58,9 @@ async def get_mandates(
|
|||
if pagination:
|
||||
try:
|
||||
paginationDict = json.loads(pagination)
|
||||
paginationParams = PaginationParams(**paginationDict) if paginationDict else None
|
||||
if paginationDict:
|
||||
paginationDict = normalize_pagination_dict(paginationDict)
|
||||
paginationParams = PaginationParams(**paginationDict)
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
|
|
@ -128,17 +130,30 @@ async def get_mandate(
|
|||
@limiter.limit("10/minute")
|
||||
async def create_mandate(
|
||||
request: Request,
|
||||
mandateData: Mandate = Body(...),
|
||||
mandateData: dict = Body(..., description="Mandate data with at least 'name' field"),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> Mandate:
|
||||
"""Create a new mandate"""
|
||||
try:
|
||||
logger.debug(f"Creating mandate with data: {mandateData}")
|
||||
|
||||
# Validate required fields
|
||||
name = mandateData.get('name')
|
||||
if not name or (isinstance(name, str) and name.strip() == ''):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Mandate name is required"
|
||||
)
|
||||
|
||||
# Get optional fields with defaults
|
||||
language = mandateData.get('language', 'en')
|
||||
|
||||
appInterface = interfaceDbAppObjects.getInterface(currentUser)
|
||||
|
||||
# Create mandate
|
||||
newMandate = appInterface.createMandate(
|
||||
name=mandateData.name,
|
||||
language=mandateData.language
|
||||
name=name,
|
||||
language=language
|
||||
)
|
||||
|
||||
if not newMandate:
|
||||
|
|
@ -162,11 +177,13 @@ async def create_mandate(
|
|||
async def update_mandate(
|
||||
request: Request,
|
||||
mandateId: str = Path(..., description="ID of the mandate to update"),
|
||||
mandateData: Mandate = Body(...),
|
||||
mandateData: dict = Body(..., description="Mandate update data"),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> Mandate:
|
||||
"""Update an existing mandate"""
|
||||
try:
|
||||
logger.debug(f"Updating mandate {mandateId} with data: {mandateData}")
|
||||
|
||||
appInterface = interfaceDbAppObjects.getInterface(currentUser)
|
||||
|
||||
# Check if mandate exists
|
||||
|
|
@ -177,8 +194,8 @@ async def update_mandate(
|
|||
detail=f"Mandate with ID {mandateId} not found"
|
||||
)
|
||||
|
||||
# Update mandate
|
||||
updatedMandate = appInterface.updateMandate(mandateId, mandateData.model_dump())
|
||||
# Update mandate - mandateData is already a dict
|
||||
updatedMandate = appInterface.updateMandate(mandateId, mandateData)
|
||||
|
||||
if not updatedMandate:
|
||||
raise HTTPException(
|
||||
|
|
|
|||
846
modules/routes/routeDataTrustee.py
Normal file
846
modules/routes/routeDataTrustee.py
Normal file
|
|
@ -0,0 +1,846 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# All rights reserved.
|
||||
"""
|
||||
Routes for Trustee feature data management.
|
||||
Implements CRUD operations for organisations, roles, access, contracts, documents, and positions.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request, Query, Response
|
||||
from fastapi.responses import StreamingResponse
|
||||
from typing import List, Dict, Any, Optional
|
||||
from fastapi import status
|
||||
import logging
|
||||
import json
|
||||
import io
|
||||
|
||||
from modules.auth import limiter, getCurrentUser
|
||||
from modules.interfaces.interfaceDbTrusteeObjects import getInterface
|
||||
from modules.datamodels.datamodelTrustee import (
|
||||
TrusteeOrganisation,
|
||||
TrusteeRole,
|
||||
TrusteeAccess,
|
||||
TrusteeContract,
|
||||
TrusteeDocument,
|
||||
TrusteePosition,
|
||||
TrusteePositionDocument,
|
||||
)
|
||||
from modules.datamodels.datamodelUam import User
|
||||
from modules.datamodels.datamodelPagination import (
|
||||
PaginationParams,
|
||||
PaginatedResponse,
|
||||
PaginationMetadata,
|
||||
normalize_pagination_dict,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/trustee",
|
||||
tags=["Trustee"],
|
||||
responses={404: {"description": "Not found"}}
|
||||
)
|
||||
|
||||
|
||||
# ===== Helper Functions =====
|
||||
|
||||
def _parsePagination(pagination: Optional[str]) -> Optional[PaginationParams]:
|
||||
"""Parse pagination parameter from JSON string."""
|
||||
if not pagination:
|
||||
return None
|
||||
try:
|
||||
paginationDict = json.loads(pagination)
|
||||
if paginationDict:
|
||||
paginationDict = normalize_pagination_dict(paginationDict)
|
||||
return PaginationParams(**paginationDict)
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid pagination parameter: {str(e)}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
# ===== Organisation Routes =====
|
||||
|
||||
@router.get("/organisations", response_model=PaginatedResponse[TrusteeOrganisation])
|
||||
@limiter.limit("30/minute")
|
||||
async def getOrganisations(
|
||||
request: Request,
|
||||
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams"),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> PaginatedResponse[TrusteeOrganisation]:
|
||||
"""Get all organisations with optional pagination."""
|
||||
paginationParams = _parsePagination(pagination)
|
||||
interface = getInterface(currentUser)
|
||||
result = interface.getAllOrganisations(paginationParams)
|
||||
|
||||
if paginationParams:
|
||||
return PaginatedResponse(
|
||||
items=result.items,
|
||||
pagination=PaginationMetadata(
|
||||
currentPage=paginationParams.page or 1,
|
||||
pageSize=paginationParams.pageSize or 20,
|
||||
totalItems=result.totalItems,
|
||||
totalPages=result.totalPages,
|
||||
sort=paginationParams.sort if paginationParams else [],
|
||||
filters=paginationParams.filters if paginationParams else None
|
||||
)
|
||||
)
|
||||
return PaginatedResponse(items=result.items, pagination=None)
|
||||
|
||||
|
||||
@router.get("/organisations/{orgId}", response_model=TrusteeOrganisation)
|
||||
@limiter.limit("30/minute")
|
||||
async def getOrganisation(
|
||||
request: Request,
|
||||
orgId: str = Path(..., description="Organisation ID"),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> TrusteeOrganisation:
|
||||
"""Get a single organisation by ID."""
|
||||
interface = getInterface(currentUser)
|
||||
org = interface.getOrganisation(orgId)
|
||||
if not org:
|
||||
raise HTTPException(status_code=404, detail=f"Organisation {orgId} not found")
|
||||
return TrusteeOrganisation(**org)
|
||||
|
||||
|
||||
@router.post("/organisations", response_model=TrusteeOrganisation, status_code=201)
|
||||
@limiter.limit("10/minute")
|
||||
async def createOrganisation(
|
||||
request: Request,
|
||||
data: TrusteeOrganisation = Body(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> TrusteeOrganisation:
|
||||
"""Create a new organisation."""
|
||||
interface = getInterface(currentUser)
|
||||
result = interface.createOrganisation(data.model_dump())
|
||||
if not result:
|
||||
raise HTTPException(status_code=400, detail="Failed to create organisation")
|
||||
return TrusteeOrganisation(**result)
|
||||
|
||||
|
||||
@router.put("/organisations/{orgId}", response_model=TrusteeOrganisation)
|
||||
@limiter.limit("10/minute")
|
||||
async def updateOrganisation(
|
||||
request: Request,
|
||||
orgId: str = Path(..., description="Organisation ID"),
|
||||
data: TrusteeOrganisation = Body(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> TrusteeOrganisation:
|
||||
"""Update an organisation."""
|
||||
interface = getInterface(currentUser)
|
||||
existing = interface.getOrganisation(orgId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Organisation {orgId} not found")
|
||||
|
||||
result = interface.updateOrganisation(orgId, data.model_dump(exclude={"id"}))
|
||||
if not result:
|
||||
raise HTTPException(status_code=400, detail="Failed to update organisation")
|
||||
return TrusteeOrganisation(**result)
|
||||
|
||||
|
||||
@router.delete("/organisations/{orgId}")
|
||||
@limiter.limit("10/minute")
|
||||
async def deleteOrganisation(
|
||||
request: Request,
|
||||
orgId: str = Path(..., description="Organisation ID"),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete an organisation."""
|
||||
interface = getInterface(currentUser)
|
||||
existing = interface.getOrganisation(orgId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Organisation {orgId} not found")
|
||||
|
||||
success = interface.deleteOrganisation(orgId)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail="Failed to delete organisation")
|
||||
return {"message": f"Organisation {orgId} deleted"}
|
||||
|
||||
|
||||
# ===== Role Routes =====
|
||||
|
||||
@router.get("/roles", response_model=PaginatedResponse[TrusteeRole])
|
||||
@limiter.limit("30/minute")
|
||||
async def getRoles(
|
||||
request: Request,
|
||||
pagination: Optional[str] = Query(None),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> PaginatedResponse[TrusteeRole]:
|
||||
"""Get all roles with optional pagination."""
|
||||
paginationParams = _parsePagination(pagination)
|
||||
interface = getInterface(currentUser)
|
||||
result = interface.getAllRoles(paginationParams)
|
||||
|
||||
if paginationParams:
|
||||
return PaginatedResponse(
|
||||
items=result.items,
|
||||
pagination=PaginationMetadata(
|
||||
currentPage=paginationParams.page or 1,
|
||||
pageSize=paginationParams.pageSize or 20,
|
||||
totalItems=result.totalItems,
|
||||
totalPages=result.totalPages,
|
||||
sort=paginationParams.sort if paginationParams else [],
|
||||
filters=paginationParams.filters if paginationParams else None
|
||||
)
|
||||
)
|
||||
return PaginatedResponse(items=result.items, pagination=None)
|
||||
|
||||
|
||||
@router.get("/roles/{roleId}", response_model=TrusteeRole)
|
||||
@limiter.limit("30/minute")
|
||||
async def getRole(
|
||||
request: Request,
|
||||
roleId: str = Path(..., description="Role ID"),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> TrusteeRole:
|
||||
"""Get a single role by ID."""
|
||||
interface = getInterface(currentUser)
|
||||
role = interface.getRole(roleId)
|
||||
if not role:
|
||||
raise HTTPException(status_code=404, detail=f"Role {roleId} not found")
|
||||
return TrusteeRole(**role)
|
||||
|
||||
|
||||
@router.post("/roles", response_model=TrusteeRole, status_code=201)
|
||||
@limiter.limit("10/minute")
|
||||
async def createRole(
|
||||
request: Request,
|
||||
data: TrusteeRole = Body(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> TrusteeRole:
|
||||
"""Create a new role (sysadmin only)."""
|
||||
interface = getInterface(currentUser)
|
||||
result = interface.createRole(data.model_dump())
|
||||
if not result:
|
||||
raise HTTPException(status_code=400, detail="Failed to create role")
|
||||
return TrusteeRole(**result)
|
||||
|
||||
|
||||
@router.put("/roles/{roleId}", response_model=TrusteeRole)
|
||||
@limiter.limit("10/minute")
|
||||
async def updateRole(
|
||||
request: Request,
|
||||
roleId: str = Path(...),
|
||||
data: TrusteeRole = Body(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> TrusteeRole:
|
||||
"""Update a role (sysadmin only)."""
|
||||
interface = getInterface(currentUser)
|
||||
existing = interface.getRole(roleId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Role {roleId} not found")
|
||||
|
||||
result = interface.updateRole(roleId, data.model_dump(exclude={"id"}))
|
||||
if not result:
|
||||
raise HTTPException(status_code=400, detail="Failed to update role")
|
||||
return TrusteeRole(**result)
|
||||
|
||||
|
||||
@router.delete("/roles/{roleId}")
|
||||
@limiter.limit("10/minute")
|
||||
async def deleteRole(
|
||||
request: Request,
|
||||
roleId: str = Path(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete a role (sysadmin only, fails if in use)."""
|
||||
interface = getInterface(currentUser)
|
||||
existing = interface.getRole(roleId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Role {roleId} not found")
|
||||
|
||||
success = interface.deleteRole(roleId)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail="Failed to delete role (may be in use)")
|
||||
return {"message": f"Role {roleId} deleted"}
|
||||
|
||||
|
||||
# ===== Access Routes =====
|
||||
|
||||
@router.get("/access", response_model=PaginatedResponse[TrusteeAccess])
|
||||
@limiter.limit("30/minute")
|
||||
async def getAllAccess(
|
||||
request: Request,
|
||||
pagination: Optional[str] = Query(None),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> PaginatedResponse[TrusteeAccess]:
|
||||
"""Get all access records with optional pagination."""
|
||||
paginationParams = _parsePagination(pagination)
|
||||
interface = getInterface(currentUser)
|
||||
result = interface.getAllAccess(paginationParams)
|
||||
|
||||
if paginationParams:
|
||||
return PaginatedResponse(
|
||||
items=result.items,
|
||||
pagination=PaginationMetadata(
|
||||
currentPage=paginationParams.page or 1,
|
||||
pageSize=paginationParams.pageSize or 20,
|
||||
totalItems=result.totalItems,
|
||||
totalPages=result.totalPages,
|
||||
sort=paginationParams.sort if paginationParams else [],
|
||||
filters=paginationParams.filters if paginationParams else None
|
||||
)
|
||||
)
|
||||
return PaginatedResponse(items=result.items, pagination=None)
|
||||
|
||||
|
||||
@router.get("/access/{accessId}", response_model=TrusteeAccess)
|
||||
@limiter.limit("30/minute")
|
||||
async def getAccess(
|
||||
request: Request,
|
||||
accessId: str = Path(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> TrusteeAccess:
|
||||
"""Get a single access record by ID."""
|
||||
interface = getInterface(currentUser)
|
||||
access = interface.getAccess(accessId)
|
||||
if not access:
|
||||
raise HTTPException(status_code=404, detail=f"Access {accessId} not found")
|
||||
return TrusteeAccess(**access)
|
||||
|
||||
|
||||
@router.get("/access/organisation/{orgId}", response_model=List[TrusteeAccess])
|
||||
@limiter.limit("30/minute")
|
||||
async def getAccessByOrganisation(
|
||||
request: Request,
|
||||
orgId: str = Path(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> List[TrusteeAccess]:
|
||||
"""Get all access records for an organisation."""
|
||||
interface = getInterface(currentUser)
|
||||
return [TrusteeAccess(**a) for a in interface.getAccessByOrganisation(orgId)]
|
||||
|
||||
|
||||
@router.get("/access/user/{userId}", response_model=List[TrusteeAccess])
|
||||
@limiter.limit("30/minute")
|
||||
async def getAccessByUser(
|
||||
request: Request,
|
||||
userId: str = Path(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> List[TrusteeAccess]:
|
||||
"""Get all access records for a user."""
|
||||
interface = getInterface(currentUser)
|
||||
return [TrusteeAccess(**a) for a in interface.getAccessByUser(userId)]
|
||||
|
||||
|
||||
@router.post("/access", response_model=TrusteeAccess, status_code=201)
|
||||
@limiter.limit("10/minute")
|
||||
async def createAccess(
|
||||
request: Request,
|
||||
data: TrusteeAccess = Body(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> TrusteeAccess:
|
||||
"""Create a new access record."""
|
||||
interface = getInterface(currentUser)
|
||||
result = interface.createAccess(data.model_dump())
|
||||
if not result:
|
||||
raise HTTPException(status_code=400, detail="Failed to create access")
|
||||
return TrusteeAccess(**result)
|
||||
|
||||
|
||||
@router.put("/access/{accessId}", response_model=TrusteeAccess)
|
||||
@limiter.limit("10/minute")
|
||||
async def updateAccess(
|
||||
request: Request,
|
||||
accessId: str = Path(...),
|
||||
data: TrusteeAccess = Body(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> TrusteeAccess:
|
||||
"""Update an access record."""
|
||||
interface = getInterface(currentUser)
|
||||
existing = interface.getAccess(accessId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Access {accessId} not found")
|
||||
|
||||
result = interface.updateAccess(accessId, data.model_dump(exclude={"id"}))
|
||||
if not result:
|
||||
raise HTTPException(status_code=400, detail="Failed to update access")
|
||||
return TrusteeAccess(**result)
|
||||
|
||||
|
||||
@router.delete("/access/{accessId}")
|
||||
@limiter.limit("10/minute")
|
||||
async def deleteAccess(
|
||||
request: Request,
|
||||
accessId: str = Path(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete an access record."""
|
||||
interface = getInterface(currentUser)
|
||||
existing = interface.getAccess(accessId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Access {accessId} not found")
|
||||
|
||||
success = interface.deleteAccess(accessId)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail="Failed to delete access")
|
||||
return {"message": f"Access {accessId} deleted"}
|
||||
|
||||
|
||||
# ===== Contract Routes =====
|
||||
|
||||
@router.get("/contracts", response_model=PaginatedResponse[TrusteeContract])
|
||||
@limiter.limit("30/minute")
|
||||
async def getContracts(
|
||||
request: Request,
|
||||
pagination: Optional[str] = Query(None),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> PaginatedResponse[TrusteeContract]:
|
||||
"""Get all contracts with optional pagination."""
|
||||
paginationParams = _parsePagination(pagination)
|
||||
interface = getInterface(currentUser)
|
||||
result = interface.getAllContracts(paginationParams)
|
||||
|
||||
if paginationParams:
|
||||
return PaginatedResponse(
|
||||
items=result.items,
|
||||
pagination=PaginationMetadata(
|
||||
currentPage=paginationParams.page or 1,
|
||||
pageSize=paginationParams.pageSize or 20,
|
||||
totalItems=result.totalItems,
|
||||
totalPages=result.totalPages,
|
||||
sort=paginationParams.sort if paginationParams else [],
|
||||
filters=paginationParams.filters if paginationParams else None
|
||||
)
|
||||
)
|
||||
return PaginatedResponse(items=result.items, pagination=None)
|
||||
|
||||
|
||||
@router.get("/contracts/{contractId}", response_model=TrusteeContract)
|
||||
@limiter.limit("30/minute")
|
||||
async def getContract(
|
||||
request: Request,
|
||||
contractId: str = Path(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> TrusteeContract:
|
||||
"""Get a single contract by ID."""
|
||||
interface = getInterface(currentUser)
|
||||
contract = interface.getContract(contractId)
|
||||
if not contract:
|
||||
raise HTTPException(status_code=404, detail=f"Contract {contractId} not found")
|
||||
return TrusteeContract(**contract)
|
||||
|
||||
|
||||
@router.get("/contracts/organisation/{orgId}", response_model=List[TrusteeContract])
|
||||
@limiter.limit("30/minute")
|
||||
async def getContractsByOrganisation(
|
||||
request: Request,
|
||||
orgId: str = Path(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> List[TrusteeContract]:
|
||||
"""Get all contracts for an organisation."""
|
||||
interface = getInterface(currentUser)
|
||||
return [TrusteeContract(**c) for c in interface.getContractsByOrganisation(orgId)]
|
||||
|
||||
|
||||
@router.post("/contracts", response_model=TrusteeContract, status_code=201)
|
||||
@limiter.limit("10/minute")
|
||||
async def createContract(
|
||||
request: Request,
|
||||
data: TrusteeContract = Body(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> TrusteeContract:
|
||||
"""Create a new contract."""
|
||||
interface = getInterface(currentUser)
|
||||
result = interface.createContract(data.model_dump())
|
||||
if not result:
|
||||
raise HTTPException(status_code=400, detail="Failed to create contract")
|
||||
return TrusteeContract(**result)
|
||||
|
||||
|
||||
@router.put("/contracts/{contractId}", response_model=TrusteeContract)
|
||||
@limiter.limit("10/minute")
|
||||
async def updateContract(
|
||||
request: Request,
|
||||
contractId: str = Path(...),
|
||||
data: TrusteeContract = Body(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> TrusteeContract:
|
||||
"""Update a contract (organisationId is immutable)."""
|
||||
interface = getInterface(currentUser)
|
||||
existing = interface.getContract(contractId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Contract {contractId} not found")
|
||||
|
||||
result = interface.updateContract(contractId, data.model_dump(exclude={"id"}))
|
||||
if not result:
|
||||
raise HTTPException(status_code=400, detail="Failed to update contract (organisationId cannot be changed)")
|
||||
return TrusteeContract(**result)
|
||||
|
||||
|
||||
@router.delete("/contracts/{contractId}")
|
||||
@limiter.limit("10/minute")
|
||||
async def deleteContract(
|
||||
request: Request,
|
||||
contractId: str = Path(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete a contract."""
|
||||
interface = getInterface(currentUser)
|
||||
existing = interface.getContract(contractId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Contract {contractId} not found")
|
||||
|
||||
success = interface.deleteContract(contractId)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail="Failed to delete contract")
|
||||
return {"message": f"Contract {contractId} deleted"}
|
||||
|
||||
|
||||
# ===== Document Routes =====
|
||||
|
||||
@router.get("/documents", response_model=PaginatedResponse[TrusteeDocument])
|
||||
@limiter.limit("30/minute")
|
||||
async def getDocuments(
|
||||
request: Request,
|
||||
pagination: Optional[str] = Query(None),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> PaginatedResponse[TrusteeDocument]:
|
||||
"""Get all documents (metadata only) with optional pagination."""
|
||||
paginationParams = _parsePagination(pagination)
|
||||
interface = getInterface(currentUser)
|
||||
result = interface.getAllDocuments(paginationParams)
|
||||
|
||||
if paginationParams:
|
||||
return PaginatedResponse(
|
||||
items=result.items,
|
||||
pagination=PaginationMetadata(
|
||||
currentPage=paginationParams.page or 1,
|
||||
pageSize=paginationParams.pageSize or 20,
|
||||
totalItems=result.totalItems,
|
||||
totalPages=result.totalPages,
|
||||
sort=paginationParams.sort if paginationParams else [],
|
||||
filters=paginationParams.filters if paginationParams else None
|
||||
)
|
||||
)
|
||||
return PaginatedResponse(items=result.items, pagination=None)
|
||||
|
||||
|
||||
@router.get("/documents/{documentId}", response_model=TrusteeDocument)
|
||||
@limiter.limit("30/minute")
|
||||
async def getDocument(
|
||||
request: Request,
|
||||
documentId: str = Path(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> TrusteeDocument:
|
||||
"""Get document metadata by ID."""
|
||||
interface = getInterface(currentUser)
|
||||
doc = interface.getDocument(documentId)
|
||||
if not doc:
|
||||
raise HTTPException(status_code=404, detail=f"Document {documentId} not found")
|
||||
return TrusteeDocument(**doc)
|
||||
|
||||
|
||||
@router.get("/documents/{documentId}/data")
|
||||
@limiter.limit("10/minute")
|
||||
async def getDocumentData(
|
||||
request: Request,
|
||||
documentId: str = Path(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
):
|
||||
"""Download document binary data."""
|
||||
interface = getInterface(currentUser)
|
||||
doc = interface.getDocument(documentId)
|
||||
if not doc:
|
||||
raise HTTPException(status_code=404, detail=f"Document {documentId} not found")
|
||||
|
||||
data = interface.getDocumentData(documentId)
|
||||
if not data:
|
||||
raise HTTPException(status_code=404, detail="Document data not found")
|
||||
|
||||
return StreamingResponse(
|
||||
io.BytesIO(data),
|
||||
media_type=doc.get("documentMimeType", "application/octet-stream"),
|
||||
headers={"Content-Disposition": f"attachment; filename={doc.get('documentName', 'document')}"}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/documents/contract/{contractId}", response_model=List[TrusteeDocument])
|
||||
@limiter.limit("30/minute")
|
||||
async def getDocumentsByContract(
|
||||
request: Request,
|
||||
contractId: str = Path(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> List[TrusteeDocument]:
|
||||
"""Get all documents for a contract."""
|
||||
interface = getInterface(currentUser)
|
||||
return [TrusteeDocument(**d) for d in interface.getDocumentsByContract(contractId)]
|
||||
|
||||
|
||||
@router.post("/documents", response_model=TrusteeDocument, status_code=201)
|
||||
@limiter.limit("10/minute")
|
||||
async def createDocument(
|
||||
request: Request,
|
||||
data: TrusteeDocument = Body(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> TrusteeDocument:
|
||||
"""Create a new document."""
|
||||
interface = getInterface(currentUser)
|
||||
result = interface.createDocument(data.model_dump())
|
||||
if not result:
|
||||
raise HTTPException(status_code=400, detail="Failed to create document")
|
||||
return TrusteeDocument(**result)
|
||||
|
||||
|
||||
@router.put("/documents/{documentId}", response_model=TrusteeDocument)
|
||||
@limiter.limit("10/minute")
|
||||
async def updateDocument(
|
||||
request: Request,
|
||||
documentId: str = Path(...),
|
||||
data: TrusteeDocument = Body(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> TrusteeDocument:
|
||||
"""Update document metadata."""
|
||||
interface = getInterface(currentUser)
|
||||
existing = interface.getDocument(documentId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Document {documentId} not found")
|
||||
|
||||
result = interface.updateDocument(documentId, data.model_dump(exclude={"id"}))
|
||||
if not result:
|
||||
raise HTTPException(status_code=400, detail="Failed to update document")
|
||||
return TrusteeDocument(**result)
|
||||
|
||||
|
||||
@router.delete("/documents/{documentId}")
|
||||
@limiter.limit("10/minute")
|
||||
async def deleteDocument(
|
||||
request: Request,
|
||||
documentId: str = Path(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete a document."""
|
||||
interface = getInterface(currentUser)
|
||||
existing = interface.getDocument(documentId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Document {documentId} not found")
|
||||
|
||||
success = interface.deleteDocument(documentId)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail="Failed to delete document")
|
||||
return {"message": f"Document {documentId} deleted"}
|
||||
|
||||
|
||||
# ===== Position Routes =====
|
||||
|
||||
@router.get("/positions", response_model=PaginatedResponse[TrusteePosition])
|
||||
@limiter.limit("30/minute")
|
||||
async def getPositions(
|
||||
request: Request,
|
||||
pagination: Optional[str] = Query(None),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> PaginatedResponse[TrusteePosition]:
|
||||
"""Get all positions with optional pagination."""
|
||||
paginationParams = _parsePagination(pagination)
|
||||
interface = getInterface(currentUser)
|
||||
result = interface.getAllPositions(paginationParams)
|
||||
|
||||
if paginationParams:
|
||||
return PaginatedResponse(
|
||||
items=result.items,
|
||||
pagination=PaginationMetadata(
|
||||
currentPage=paginationParams.page or 1,
|
||||
pageSize=paginationParams.pageSize or 20,
|
||||
totalItems=result.totalItems,
|
||||
totalPages=result.totalPages,
|
||||
sort=paginationParams.sort if paginationParams else [],
|
||||
filters=paginationParams.filters if paginationParams else None
|
||||
)
|
||||
)
|
||||
return PaginatedResponse(items=result.items, pagination=None)
|
||||
|
||||
|
||||
@router.get("/positions/{positionId}", response_model=TrusteePosition)
|
||||
@limiter.limit("30/minute")
|
||||
async def getPosition(
|
||||
request: Request,
|
||||
positionId: str = Path(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> TrusteePosition:
|
||||
"""Get a single position by ID."""
|
||||
interface = getInterface(currentUser)
|
||||
position = interface.getPosition(positionId)
|
||||
if not position:
|
||||
raise HTTPException(status_code=404, detail=f"Position {positionId} not found")
|
||||
return TrusteePosition(**position)
|
||||
|
||||
|
||||
@router.get("/positions/contract/{contractId}", response_model=List[TrusteePosition])
|
||||
@limiter.limit("30/minute")
|
||||
async def getPositionsByContract(
|
||||
request: Request,
|
||||
contractId: str = Path(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> List[TrusteePosition]:
|
||||
"""Get all positions for a contract."""
|
||||
interface = getInterface(currentUser)
|
||||
return [TrusteePosition(**p) for p in interface.getPositionsByContract(contractId)]
|
||||
|
||||
|
||||
@router.get("/positions/organisation/{orgId}", response_model=List[TrusteePosition])
|
||||
@limiter.limit("30/minute")
|
||||
async def getPositionsByOrganisation(
|
||||
request: Request,
|
||||
orgId: str = Path(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> List[TrusteePosition]:
|
||||
"""Get all positions for an organisation."""
|
||||
interface = getInterface(currentUser)
|
||||
return [TrusteePosition(**p) for p in interface.getPositionsByOrganisation(orgId)]
|
||||
|
||||
|
||||
@router.post("/positions", response_model=TrusteePosition, status_code=201)
|
||||
@limiter.limit("10/minute")
|
||||
async def createPosition(
|
||||
request: Request,
|
||||
data: TrusteePosition = Body(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> TrusteePosition:
|
||||
"""Create a new position."""
|
||||
interface = getInterface(currentUser)
|
||||
result = interface.createPosition(data.model_dump())
|
||||
if not result:
|
||||
raise HTTPException(status_code=400, detail="Failed to create position")
|
||||
return TrusteePosition(**result)
|
||||
|
||||
|
||||
@router.put("/positions/{positionId}", response_model=TrusteePosition)
|
||||
@limiter.limit("10/minute")
|
||||
async def updatePosition(
|
||||
request: Request,
|
||||
positionId: str = Path(...),
|
||||
data: TrusteePosition = Body(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> TrusteePosition:
|
||||
"""Update a position."""
|
||||
interface = getInterface(currentUser)
|
||||
existing = interface.getPosition(positionId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Position {positionId} not found")
|
||||
|
||||
result = interface.updatePosition(positionId, data.model_dump(exclude={"id"}))
|
||||
if not result:
|
||||
raise HTTPException(status_code=400, detail="Failed to update position")
|
||||
return TrusteePosition(**result)
|
||||
|
||||
|
||||
@router.delete("/positions/{positionId}")
|
||||
@limiter.limit("10/minute")
|
||||
async def deletePosition(
|
||||
request: Request,
|
||||
positionId: str = Path(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete a position."""
|
||||
interface = getInterface(currentUser)
|
||||
existing = interface.getPosition(positionId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Position {positionId} not found")
|
||||
|
||||
success = interface.deletePosition(positionId)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail="Failed to delete position")
|
||||
return {"message": f"Position {positionId} deleted"}
|
||||
|
||||
|
||||
# ===== Position-Document Link Routes =====
|
||||
|
||||
@router.get("/position-documents", response_model=PaginatedResponse[TrusteePositionDocument])
|
||||
@limiter.limit("30/minute")
|
||||
async def getPositionDocuments(
|
||||
request: Request,
|
||||
pagination: Optional[str] = Query(None),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> PaginatedResponse[TrusteePositionDocument]:
|
||||
"""Get all position-document links with optional pagination."""
|
||||
paginationParams = _parsePagination(pagination)
|
||||
interface = getInterface(currentUser)
|
||||
result = interface.getAllPositionDocuments(paginationParams)
|
||||
|
||||
if paginationParams:
|
||||
return PaginatedResponse(
|
||||
items=result.items,
|
||||
pagination=PaginationMetadata(
|
||||
currentPage=paginationParams.page or 1,
|
||||
pageSize=paginationParams.pageSize or 20,
|
||||
totalItems=result.totalItems,
|
||||
totalPages=result.totalPages,
|
||||
sort=paginationParams.sort if paginationParams else [],
|
||||
filters=paginationParams.filters if paginationParams else None
|
||||
)
|
||||
)
|
||||
return PaginatedResponse(items=result.items, pagination=None)
|
||||
|
||||
|
||||
@router.get("/position-documents/{linkId}", response_model=TrusteePositionDocument)
|
||||
@limiter.limit("30/minute")
|
||||
async def getPositionDocument(
|
||||
request: Request,
|
||||
linkId: str = Path(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> TrusteePositionDocument:
|
||||
"""Get a single position-document link by ID."""
|
||||
interface = getInterface(currentUser)
|
||||
link = interface.getPositionDocument(linkId)
|
||||
if not link:
|
||||
raise HTTPException(status_code=404, detail=f"Link {linkId} not found")
|
||||
return TrusteePositionDocument(**link)
|
||||
|
||||
|
||||
@router.get("/position-documents/position/{positionId}", response_model=List[TrusteePositionDocument])
|
||||
@limiter.limit("30/minute")
|
||||
async def getDocumentsForPosition(
|
||||
request: Request,
|
||||
positionId: str = Path(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> List[TrusteePositionDocument]:
|
||||
"""Get all document links for a position."""
|
||||
interface = getInterface(currentUser)
|
||||
return [TrusteePositionDocument(**l) for l in interface.getDocumentsForPosition(positionId)]
|
||||
|
||||
|
||||
@router.get("/position-documents/document/{documentId}", response_model=List[TrusteePositionDocument])
|
||||
@limiter.limit("30/minute")
|
||||
async def getPositionsForDocument(
|
||||
request: Request,
|
||||
documentId: str = Path(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> List[TrusteePositionDocument]:
|
||||
"""Get all position links for a document."""
|
||||
interface = getInterface(currentUser)
|
||||
return [TrusteePositionDocument(**l) for l in interface.getPositionsForDocument(documentId)]
|
||||
|
||||
|
||||
@router.post("/position-documents", response_model=TrusteePositionDocument, status_code=201)
|
||||
@limiter.limit("10/minute")
|
||||
async def createPositionDocument(
|
||||
request: Request,
|
||||
data: TrusteePositionDocument = Body(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> TrusteePositionDocument:
|
||||
"""Create a new position-document link."""
|
||||
interface = getInterface(currentUser)
|
||||
result = interface.createPositionDocument(data.model_dump())
|
||||
if not result:
|
||||
raise HTTPException(status_code=400, detail="Failed to create link")
|
||||
return TrusteePositionDocument(**result)
|
||||
|
||||
|
||||
@router.delete("/position-documents/{linkId}")
|
||||
@limiter.limit("10/minute")
|
||||
async def deletePositionDocument(
|
||||
request: Request,
|
||||
linkId: str = Path(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete a position-document link."""
|
||||
interface = getInterface(currentUser)
|
||||
existing = interface.getPositionDocument(linkId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Link {linkId} not found")
|
||||
|
||||
success = interface.deletePositionDocument(linkId)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail="Failed to delete link")
|
||||
return {"message": f"Link {linkId} deleted"}
|
||||
|
|
@ -17,7 +17,7 @@ from modules.auth import getCurrentUser, limiter
|
|||
|
||||
# Import the attribute definition and helper functions
|
||||
from modules.datamodels.datamodelUam import User
|
||||
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata
|
||||
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -53,7 +53,9 @@ async def get_users(
|
|||
if pagination:
|
||||
try:
|
||||
paginationDict = json.loads(pagination)
|
||||
paginationParams = PaginationParams(**paginationDict) if paginationDict else None
|
||||
if paginationDict:
|
||||
paginationDict = normalize_pagination_dict(paginationDict)
|
||||
paginationParams = PaginationParams(**paginationDict)
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
|
|
@ -338,6 +340,131 @@ async def change_password(
|
|||
detail=f"Password change failed: {str(e)}"
|
||||
)
|
||||
|
||||
@router.post("/{userId}/send-password-link")
|
||||
@limiter.limit("10/minute")
|
||||
async def sendPasswordLink(
|
||||
request: Request,
|
||||
userId: str = Path(..., description="ID of the user to send password setup link"),
|
||||
frontendUrl: str = Body(..., embed=True),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> Dict[str, Any]:
|
||||
"""Send password setup/reset link to a user (admin function).
|
||||
|
||||
This allows admins to send a magic link to users to set or reset their password.
|
||||
Used when creating users without password or to help users who forgot their password.
|
||||
|
||||
Args:
|
||||
userId: ID of the user to send the link to
|
||||
frontendUrl: The frontend URL to use in the magic link
|
||||
"""
|
||||
try:
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
from modules.interfaces.interfaceDbAppObjects import getRootInterface
|
||||
|
||||
# Get user interface
|
||||
appInterface = interfaceDbAppObjects.getInterface(currentUser)
|
||||
|
||||
# Get target user
|
||||
targetUser = appInterface.getUser(userId)
|
||||
if not targetUser:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
# Check if user has an email
|
||||
if not targetUser.email:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="User has no email address configured"
|
||||
)
|
||||
|
||||
# Use root interface for token operations
|
||||
rootInterface = getRootInterface()
|
||||
|
||||
# Generate reset token
|
||||
token, expires = rootInterface.generateResetTokenAndExpiry()
|
||||
|
||||
# Set reset token (don't clear password - user might have one already)
|
||||
rootInterface.setResetToken(userId, token, expires, clearPassword=False)
|
||||
|
||||
# Send email with magic link
|
||||
baseUrl = frontendUrl.rstrip("/")
|
||||
magicLink = f"{baseUrl}/reset?token={token}"
|
||||
expiryHours = int(APP_CONFIG.get("Auth_RESET_TOKEN_EXPIRY_HOURS", "24"))
|
||||
|
||||
try:
|
||||
from modules.services import Services
|
||||
services = Services(targetUser)
|
||||
|
||||
emailSubject = "PowerOn - Passwort setzen"
|
||||
emailBody = f"""
|
||||
Hallo {targetUser.fullName or targetUser.username},
|
||||
|
||||
Ein Administrator hat einen Link zum Setzen Ihres Passworts angefordert.
|
||||
|
||||
Ihr Benutzername: {targetUser.username}
|
||||
|
||||
Klicken Sie auf den folgenden Link, um Ihr Passwort zu setzen:
|
||||
{magicLink}
|
||||
|
||||
Dieser Link ist {expiryHours} Stunden gültig.
|
||||
|
||||
Falls Sie diese Anforderung nicht erwartet haben, kontaktieren Sie bitte Ihren Administrator.
|
||||
"""
|
||||
|
||||
emailSent = services.messaging.sendEmailDirect(
|
||||
recipient=targetUser.email,
|
||||
subject=emailSubject,
|
||||
message=emailBody,
|
||||
userId=str(targetUser.id)
|
||||
)
|
||||
|
||||
if not emailSent:
|
||||
logger.warning(f"Failed to send password setup email to {targetUser.email}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to send email"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as emailErr:
|
||||
logger.error(f"Error sending password setup email: {str(emailErr)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to send email: {str(emailErr)}"
|
||||
)
|
||||
|
||||
# Log the action
|
||||
try:
|
||||
from modules.shared.auditLogger import audit_logger
|
||||
audit_logger.logSecurityEvent(
|
||||
userId=str(currentUser.id),
|
||||
mandateId=str(currentUser.mandateId),
|
||||
action="send_password_link",
|
||||
details=f"Sent password setup link to user {userId} ({targetUser.email})"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.info(f"Password setup link sent to {targetUser.email} for user {targetUser.username} by admin {currentUser.username}")
|
||||
|
||||
return {
|
||||
"message": f"Password setup link sent to {targetUser.email}",
|
||||
"userId": userId,
|
||||
"email": targetUser.email
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending password link: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to send password link: {str(e)}"
|
||||
)
|
||||
|
||||
@router.delete("/{userId}", response_model=Dict[str, Any])
|
||||
@limiter.limit("10/minute")
|
||||
async def delete_user(
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import math
|
|||
from modules.auth import getCurrentUser, limiter
|
||||
from modules.datamodels.datamodelUam import User, UserPermissions, AccessLevel
|
||||
from modules.datamodels.datamodelRbac import AccessRuleContext, AccessRule, Role
|
||||
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata
|
||||
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
|
||||
from modules.interfaces.interfaceDbAppObjects import getInterface
|
||||
|
||||
# Configure logger
|
||||
|
|
@ -28,7 +28,7 @@ router = APIRouter(
|
|||
|
||||
|
||||
@router.get("/permissions", response_model=UserPermissions)
|
||||
@limiter.limit("60/minute")
|
||||
@limiter.limit("300/minute") # Raised from 60 - sidebar checks many pages individually
|
||||
async def getPermissions(
|
||||
request: Request,
|
||||
context: str = Query(..., description="Context type: DATA, UI, or RESOURCE"),
|
||||
|
|
@ -90,7 +90,7 @@ async def getPermissions(
|
|||
|
||||
|
||||
@router.get("/permissions/all", response_model=Dict[str, Any])
|
||||
@limiter.limit("30/minute")
|
||||
@limiter.limit("120/minute") # Raised from 30 - optimized endpoint for bulk permission fetch
|
||||
async def getAllPermissions(
|
||||
request: Request,
|
||||
context: Optional[str] = Query(None, description="Context type: UI or RESOURCE (if not provided, returns both)"),
|
||||
|
|
@ -277,7 +277,9 @@ async def getAccessRules(
|
|||
if pagination:
|
||||
try:
|
||||
paginationDict = json.loads(pagination)
|
||||
paginationParams = PaginationParams(**paginationDict) if paginationDict else None
|
||||
if paginationDict:
|
||||
paginationDict = normalize_pagination_dict(paginationDict)
|
||||
paginationParams = PaginationParams(**paginationDict)
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
|
|
@ -427,18 +429,26 @@ async def createAccessRule(
|
|||
|
||||
# Validate and parse access rule data
|
||||
try:
|
||||
logger.debug(f"Creating access rule with data: {accessRuleData}")
|
||||
|
||||
# Parse context if provided as string
|
||||
if "context" in accessRuleData and isinstance(accessRuleData["context"], str):
|
||||
accessRuleData["context"] = AccessRuleContext(accessRuleData["context"].upper())
|
||||
|
||||
# Parse AccessLevel fields if provided as strings
|
||||
# Handle empty strings by converting to None
|
||||
for field in ["read", "create", "update", "delete"]:
|
||||
if field in accessRuleData and isinstance(accessRuleData[field], str):
|
||||
accessRuleData[field] = AccessLevel(accessRuleData[field])
|
||||
if field in accessRuleData:
|
||||
value = accessRuleData[field]
|
||||
if value == "" or value is None:
|
||||
accessRuleData[field] = None
|
||||
elif isinstance(value, str):
|
||||
accessRuleData[field] = AccessLevel(value)
|
||||
|
||||
# Create AccessRule object
|
||||
accessRule = AccessRule(**accessRuleData)
|
||||
except ValueError as e:
|
||||
logger.error(f"Invalid access rule data: {accessRuleData} - Error: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid access rule data: {str(e)}"
|
||||
|
|
@ -526,9 +536,14 @@ async def updateAccessRule(
|
|||
updateData["context"] = AccessRuleContext(updateData["context"].upper())
|
||||
|
||||
# Parse AccessLevel fields if provided as strings
|
||||
# Handle empty strings by converting to None
|
||||
for field in ["read", "create", "update", "delete"]:
|
||||
if field in updateData and isinstance(updateData[field], str):
|
||||
updateData[field] = AccessLevel(updateData[field])
|
||||
if field in updateData:
|
||||
value = updateData[field]
|
||||
if value == "" or value is None:
|
||||
updateData[field] = None
|
||||
elif isinstance(value, str):
|
||||
updateData[field] = AccessLevel(value)
|
||||
|
||||
# Ensure ID is set correctly
|
||||
updateData["id"] = ruleId
|
||||
|
|
@ -536,6 +551,7 @@ async def updateAccessRule(
|
|||
# Create AccessRule object
|
||||
accessRule = AccessRule(**updateData)
|
||||
except ValueError as e:
|
||||
logger.error(f"Invalid access rule update data: {updateData} - Error: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid access rule data: {str(e)}"
|
||||
|
|
@ -671,7 +687,9 @@ async def listRoles(
|
|||
if pagination:
|
||||
try:
|
||||
paginationDict = json.loads(pagination)
|
||||
paginationParams = PaginationParams(**paginationDict) if paginationDict else None
|
||||
if paginationDict:
|
||||
paginationDict = normalize_pagination_dict(paginationDict)
|
||||
paginationParams = PaginationParams(**paginationDict)
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
|
|
|
|||
|
|
@ -84,30 +84,17 @@ async def get_workflows(
|
|||
appInterface = getInterface(currentUser)
|
||||
result = appInterface.getWorkflows(pagination=paginationParams)
|
||||
|
||||
# Convert raw dictionaries to ChatWorkflow objects by loading each workflow properly
|
||||
# If pagination was requested, result is PaginatedResult with items as dicts
|
||||
# If no pagination, result is List[Dict]
|
||||
if paginationParams:
|
||||
workflows_data = result.items
|
||||
workflows = result.items
|
||||
totalItems = result.totalItems
|
||||
totalPages = result.totalPages
|
||||
else:
|
||||
workflows_data = result
|
||||
workflows = result
|
||||
totalItems = len(result)
|
||||
totalPages = 1
|
||||
|
||||
workflows = []
|
||||
for workflow_data in workflows_data:
|
||||
try:
|
||||
# Load the workflow properly using the same method as individual workflow endpoint
|
||||
workflow = appInterface.getWorkflow(workflow_data["id"])
|
||||
if workflow:
|
||||
workflows.append(workflow)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error loading workflow {workflow_data.get('id', 'unknown')}: {str(e)}")
|
||||
# Skip invalid workflows instead of failing the entire request
|
||||
continue
|
||||
|
||||
if paginationParams:
|
||||
return PaginatedResponse(
|
||||
items=workflows,
|
||||
|
|
|
|||
Loading…
Reference in a new issue