fixed automation and trustee
This commit is contained in:
parent
a0b9a6e4b5
commit
50e3fce12b
8 changed files with 296 additions and 35 deletions
3
app.py
3
app.py
|
|
@ -47,6 +47,9 @@ class DailyRotatingFileHandler(RotatingFileHandler):
|
|||
|
||||
def _updateFileIfNeeded(self):
|
||||
"""Update the log file if the date has changed"""
|
||||
# Guard against interpreter shutdown when datetime may be None
|
||||
if datetime is None:
|
||||
return False
|
||||
today = datetime.now().strftime("%Y%m%d")
|
||||
|
||||
if self.currentDate != today:
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import json
|
|||
|
||||
# Import interfaces and models
|
||||
from modules.interfaces.interfaceDbChat import getInterface as getChatInterface
|
||||
from modules.auth import getCurrentUser, limiter
|
||||
from modules.auth import getCurrentUser, limiter, getRequestContext, RequestContext
|
||||
from modules.features.automation.datamodelFeatureAutomation import AutomationDefinition
|
||||
from modules.datamodels.datamodelChat import ChatWorkflow
|
||||
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
|
||||
|
|
@ -46,7 +46,7 @@ router = APIRouter(
|
|||
async def get_automations(
|
||||
request: Request,
|
||||
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
|
||||
currentUser = Depends(getCurrentUser)
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> PaginatedResponse[AutomationDefinition]:
|
||||
"""
|
||||
Get automation definitions with optional pagination, sorting, and filtering.
|
||||
|
|
@ -69,7 +69,7 @@ async def get_automations(
|
|||
detail=f"Invalid pagination parameter: {str(e)}"
|
||||
)
|
||||
|
||||
chatInterface = getChatInterface(currentUser)
|
||||
chatInterface = getChatInterface(context.user, mandateId=str(context.mandateId) if context.mandateId else None, featureInstanceId=str(context.featureInstanceId) if context.featureInstanceId else None)
|
||||
result = chatInterface.getAllAutomationDefinitions(pagination=paginationParams)
|
||||
|
||||
# If pagination was requested, result is PaginatedResult
|
||||
|
|
@ -111,11 +111,11 @@ async def get_automations(
|
|||
async def create_automation(
|
||||
request: Request,
|
||||
automation: AutomationDefinition,
|
||||
currentUser = Depends(getCurrentUser)
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> AutomationDefinition:
|
||||
"""Create a new automation definition"""
|
||||
try:
|
||||
chatInterface = getChatInterface(currentUser)
|
||||
chatInterface = getChatInterface(context.user, mandateId=str(context.mandateId) if context.mandateId else None, featureInstanceId=str(context.featureInstanceId) if context.featureInstanceId else None)
|
||||
automationData = automation.model_dump()
|
||||
created = chatInterface.createAutomationDefinition(automationData)
|
||||
return created
|
||||
|
|
@ -132,7 +132,7 @@ async def create_automation(
|
|||
@limiter.limit("30/minute")
|
||||
async def get_automation_templates(
|
||||
request: Request,
|
||||
currentUser = Depends(getCurrentUser)
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
Get automation templates from backend module.
|
||||
|
|
@ -160,11 +160,11 @@ async def get_automation_attributes(
|
|||
async def get_automation(
|
||||
request: Request,
|
||||
automationId: str = Path(..., description="Automation ID"),
|
||||
currentUser = Depends(getCurrentUser)
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> AutomationDefinition:
|
||||
"""Get a single automation definition by ID"""
|
||||
try:
|
||||
chatInterface = getChatInterface(currentUser)
|
||||
chatInterface = getChatInterface(context.user, mandateId=str(context.mandateId) if context.mandateId else None, featureInstanceId=str(context.featureInstanceId) if context.featureInstanceId else None)
|
||||
automation = chatInterface.getAutomationDefinition(automationId)
|
||||
if not automation:
|
||||
raise HTTPException(
|
||||
|
|
@ -188,11 +188,11 @@ async def update_automation(
|
|||
request: Request,
|
||||
automationId: str = Path(..., description="Automation ID"),
|
||||
automation: AutomationDefinition = Body(...),
|
||||
currentUser = Depends(getCurrentUser)
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> AutomationDefinition:
|
||||
"""Update an automation definition"""
|
||||
try:
|
||||
chatInterface = getChatInterface(currentUser)
|
||||
chatInterface = getChatInterface(context.user, mandateId=str(context.mandateId) if context.mandateId else None, featureInstanceId=str(context.featureInstanceId) if context.featureInstanceId else None)
|
||||
automationData = automation.model_dump()
|
||||
updated = chatInterface.updateAutomationDefinition(automationId, automationData)
|
||||
return updated
|
||||
|
|
@ -215,11 +215,11 @@ async def update_automation(
|
|||
async def delete_automation(
|
||||
request: Request,
|
||||
automationId: str = Path(..., description="Automation ID"),
|
||||
currentUser = Depends(getCurrentUser)
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> Response:
|
||||
"""Delete an automation definition"""
|
||||
try:
|
||||
chatInterface = getChatInterface(currentUser)
|
||||
chatInterface = getChatInterface(context.user, mandateId=str(context.mandateId) if context.mandateId else None, featureInstanceId=str(context.featureInstanceId) if context.featureInstanceId else None)
|
||||
success = chatInterface.deleteAutomationDefinition(automationId)
|
||||
if success:
|
||||
return Response(status_code=204)
|
||||
|
|
@ -244,15 +244,15 @@ async def delete_automation(
|
|||
|
||||
@router.post("/{automationId}/execute", response_model=ChatWorkflow)
|
||||
@limiter.limit("5/minute")
|
||||
async def execute_automation(
|
||||
async def execute_automation_route(
|
||||
request: Request,
|
||||
automationId: str = Path(..., description="Automation ID"),
|
||||
currentUser = Depends(getCurrentUser)
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> ChatWorkflow:
|
||||
"""Execute an automation immediately (test mode)"""
|
||||
try:
|
||||
from modules.services import getInterface as getServices
|
||||
services = getServices(currentUser, None)
|
||||
services = getServices(context.user, context.mandateId)
|
||||
workflow = await executeAutomation(automationId, services)
|
||||
return workflow
|
||||
except HTTPException:
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ def discoverFeatureContainers() -> List[str]:
|
|||
def loadFeatureRouters(app: FastAPI) -> Dict[str, Any]:
|
||||
"""
|
||||
Dynamically load and register routers from all discovered feature containers.
|
||||
Also registers feature template roles and AccessRules in the database.
|
||||
"""
|
||||
results = {}
|
||||
pattern = os.path.join(FEATURES_DIR, "*", "routeFeature*.py")
|
||||
|
|
@ -64,6 +65,15 @@ def loadFeatureRouters(app: FastAPI) -> Dict[str, Any]:
|
|||
logger.error(f"Failed to load router from {featureDir}: {e}")
|
||||
results[featureDir] = {"status": "error", "error": str(e)}
|
||||
|
||||
# Register features in RBAC catalog and sync template roles to database
|
||||
from modules.security.rbacCatalog import getCatalogService
|
||||
catalogService = getCatalogService()
|
||||
registrationResults = registerAllFeaturesInCatalog(catalogService)
|
||||
|
||||
for featureName, success in registrationResults.items():
|
||||
if featureName in results:
|
||||
results[featureName]["rbac_registered"] = success
|
||||
|
||||
return results
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -103,7 +103,8 @@ RESOURCE_OBJECTS = [
|
|||
},
|
||||
]
|
||||
|
||||
# Template roles for this feature
|
||||
# Template roles for this feature with AccessRules
|
||||
# Each role defines default UI and DATA permissions
|
||||
TEMPLATE_ROLES = [
|
||||
{
|
||||
"roleLabel": "trustee-admin",
|
||||
|
|
@ -111,7 +112,13 @@ TEMPLATE_ROLES = [
|
|||
"en": "Trustee Administrator - Full access to all trustee data and settings",
|
||||
"de": "Treuhand-Administrator - Vollzugriff auf alle Treuhand-Daten und Einstellungen",
|
||||
"fr": "Administrateur fiduciaire - Accès complet aux données et paramètres fiduciaires"
|
||||
}
|
||||
},
|
||||
"accessRules": [
|
||||
# Full UI access
|
||||
{"context": "UI", "item": None, "view": True},
|
||||
# Full DATA access
|
||||
{"context": "DATA", "item": None, "view": True, "read": "a", "create": "a", "update": "a", "delete": "a"},
|
||||
]
|
||||
},
|
||||
{
|
||||
"roleLabel": "trustee-accountant",
|
||||
|
|
@ -119,7 +126,13 @@ TEMPLATE_ROLES = [
|
|||
"en": "Trustee Accountant - Manage accounting and financial data",
|
||||
"de": "Treuhand-Buchhalter - Buchhaltungs- und Finanzdaten verwalten",
|
||||
"fr": "Comptable fiduciaire - Gérer les données comptables et financières"
|
||||
}
|
||||
},
|
||||
"accessRules": [
|
||||
# Full UI access
|
||||
{"context": "UI", "item": None, "view": True},
|
||||
# Group-level DATA access
|
||||
{"context": "DATA", "item": None, "view": True, "read": "g", "create": "g", "update": "g", "delete": "g"},
|
||||
]
|
||||
},
|
||||
{
|
||||
"roleLabel": "trustee-client",
|
||||
|
|
@ -127,7 +140,13 @@ TEMPLATE_ROLES = [
|
|||
"en": "Trustee Client - View own accounting data and documents",
|
||||
"de": "Treuhand-Kunde - Eigene Buchhaltungsdaten und Dokumente einsehen",
|
||||
"fr": "Client fiduciaire - Consulter ses propres données comptables et documents"
|
||||
}
|
||||
},
|
||||
"accessRules": [
|
||||
# Full UI access
|
||||
{"context": "UI", "item": None, "view": True},
|
||||
# Own records only (MY level)
|
||||
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
|
|
@ -185,9 +204,198 @@ def registerFeature(catalogService) -> bool:
|
|||
meta=resObj.get("meta")
|
||||
)
|
||||
|
||||
# Sync template roles to database (with AccessRules)
|
||||
_syncTemplateRolesToDb()
|
||||
|
||||
logger.info(f"Feature '{FEATURE_CODE}' registered {len(UI_OBJECTS)} UI objects and {len(RESOURCE_OBJECTS)} resource objects")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to register feature '{FEATURE_CODE}': {e}")
|
||||
return False
|
||||
|
||||
|
||||
def _syncTemplateRolesToDb() -> int:
|
||||
"""
|
||||
Sync template roles and their AccessRules to the database.
|
||||
Creates global template roles (mandateId=None) if they don't exist.
|
||||
|
||||
Returns:
|
||||
Number of roles created/updated
|
||||
"""
|
||||
try:
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
|
||||
|
||||
rootInterface = getRootInterface()
|
||||
db = rootInterface.db
|
||||
|
||||
# Get existing template roles for this feature
|
||||
existingRoles = db.getRecordset(
|
||||
Role,
|
||||
recordFilter={"featureCode": FEATURE_CODE, "mandateId": None}
|
||||
)
|
||||
existingRoleLabels = {r.get("roleLabel"): r.get("id") for r in existingRoles}
|
||||
|
||||
createdCount = 0
|
||||
for roleTemplate in TEMPLATE_ROLES:
|
||||
roleLabel = roleTemplate["roleLabel"]
|
||||
|
||||
if roleLabel in existingRoleLabels:
|
||||
roleId = existingRoleLabels[roleLabel]
|
||||
logger.debug(f"Template role '{roleLabel}' already exists with ID {roleId}")
|
||||
|
||||
# Ensure AccessRules exist for this role
|
||||
_ensureAccessRulesForRole(db, roleId, roleTemplate.get("accessRules", []))
|
||||
else:
|
||||
# Create new template role
|
||||
newRole = Role(
|
||||
roleLabel=roleLabel,
|
||||
description=roleTemplate.get("description", {}),
|
||||
featureCode=FEATURE_CODE,
|
||||
mandateId=None, # Global template
|
||||
featureInstanceId=None,
|
||||
isSystemRole=False
|
||||
)
|
||||
createdRole = db.recordCreate(Role, newRole.model_dump())
|
||||
roleId = createdRole.get("id")
|
||||
|
||||
# Create AccessRules for this role
|
||||
_ensureAccessRulesForRole(db, roleId, roleTemplate.get("accessRules", []))
|
||||
|
||||
logger.info(f"Created template role '{roleLabel}' with ID {roleId}")
|
||||
createdCount += 1
|
||||
|
||||
if createdCount > 0:
|
||||
logger.info(f"Feature '{FEATURE_CODE}': Created {createdCount} template roles")
|
||||
|
||||
# Repair instance-specific roles that are missing AccessRules
|
||||
_repairInstanceRolesAccessRules(db, existingRoleLabels)
|
||||
|
||||
return createdCount
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error syncing template roles for feature '{FEATURE_CODE}': {e}")
|
||||
return 0
|
||||
|
||||
|
||||
def _repairInstanceRolesAccessRules(db, templateRoleLabels: Dict[str, str]) -> int:
|
||||
"""
|
||||
Repair instance-specific roles by copying AccessRules from their template roles.
|
||||
This ensures instance roles created before AccessRules were defined get updated.
|
||||
|
||||
Args:
|
||||
db: Database connector
|
||||
templateRoleLabels: Dict mapping roleLabel to template role ID
|
||||
|
||||
Returns:
|
||||
Number of instance roles repaired
|
||||
"""
|
||||
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
|
||||
|
||||
repairedCount = 0
|
||||
|
||||
# Get all instance-specific roles for this feature (mandateId is NOT None)
|
||||
allRoles = db.getRecordset(Role, recordFilter={"featureCode": FEATURE_CODE})
|
||||
instanceRoles = [r for r in allRoles if r.get("mandateId") is not None]
|
||||
|
||||
for instanceRole in instanceRoles:
|
||||
roleLabel = instanceRole.get("roleLabel")
|
||||
instanceRoleId = instanceRole.get("id")
|
||||
|
||||
# Find matching template role
|
||||
templateRoleId = templateRoleLabels.get(roleLabel)
|
||||
if not templateRoleId:
|
||||
continue
|
||||
|
||||
# Check if instance role has AccessRules
|
||||
existingRules = db.getRecordset(AccessRule, recordFilter={"roleId": instanceRoleId})
|
||||
if existingRules:
|
||||
continue # Already has rules, skip
|
||||
|
||||
# Copy AccessRules from template role
|
||||
templateRules = db.getRecordset(AccessRule, recordFilter={"roleId": templateRoleId})
|
||||
if not templateRules:
|
||||
continue # Template has no rules
|
||||
|
||||
for rule in templateRules:
|
||||
newRule = AccessRule(
|
||||
roleId=instanceRoleId,
|
||||
context=rule.get("context"),
|
||||
item=rule.get("item"),
|
||||
view=rule.get("view", False),
|
||||
read=rule.get("read"),
|
||||
create=rule.get("create"),
|
||||
update=rule.get("update"),
|
||||
delete=rule.get("delete"),
|
||||
)
|
||||
db.recordCreate(AccessRule, newRule.model_dump())
|
||||
|
||||
logger.info(f"Repaired instance role '{roleLabel}' (ID: {instanceRoleId}): copied {len(templateRules)} AccessRules from template")
|
||||
repairedCount += 1
|
||||
|
||||
if repairedCount > 0:
|
||||
logger.info(f"Feature '{FEATURE_CODE}': Repaired {repairedCount} instance roles with missing AccessRules")
|
||||
|
||||
return repairedCount
|
||||
|
||||
|
||||
def _ensureAccessRulesForRole(db, roleId: str, ruleTemplates: List[Dict[str, Any]]) -> int:
|
||||
"""
|
||||
Ensure AccessRules exist for a role based on templates.
|
||||
|
||||
Args:
|
||||
db: Database connector
|
||||
roleId: Role ID
|
||||
ruleTemplates: List of rule templates
|
||||
|
||||
Returns:
|
||||
Number of rules created
|
||||
"""
|
||||
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
|
||||
|
||||
# Get existing rules for this role
|
||||
existingRules = db.getRecordset(AccessRule, recordFilter={"roleId": roleId})
|
||||
|
||||
# Create a set of existing rule signatures to avoid duplicates
|
||||
existingSignatures = set()
|
||||
for rule in existingRules:
|
||||
sig = (rule.get("context"), rule.get("item"))
|
||||
existingSignatures.add(sig)
|
||||
|
||||
createdCount = 0
|
||||
for template in ruleTemplates:
|
||||
context = template.get("context", "UI")
|
||||
item = template.get("item")
|
||||
sig = (context, item)
|
||||
|
||||
if sig in existingSignatures:
|
||||
continue
|
||||
|
||||
# Map context string to enum
|
||||
if context == "UI":
|
||||
contextEnum = AccessRuleContext.UI
|
||||
elif context == "DATA":
|
||||
contextEnum = AccessRuleContext.DATA
|
||||
elif context == "RESOURCE":
|
||||
contextEnum = AccessRuleContext.RESOURCE
|
||||
else:
|
||||
contextEnum = context
|
||||
|
||||
newRule = AccessRule(
|
||||
roleId=roleId,
|
||||
context=contextEnum,
|
||||
item=item,
|
||||
view=template.get("view", False),
|
||||
read=template.get("read"),
|
||||
create=template.get("create"),
|
||||
update=template.get("update"),
|
||||
delete=template.get("delete"),
|
||||
)
|
||||
db.recordCreate(AccessRule, newRule.model_dump())
|
||||
createdCount += 1
|
||||
|
||||
if createdCount > 0:
|
||||
logger.debug(f"Created {createdCount} AccessRules for role {roleId}")
|
||||
|
||||
return createdCount
|
||||
|
|
|
|||
|
|
@ -1729,8 +1729,14 @@ class ChatObjects:
|
|||
totalPages=totalPages
|
||||
)
|
||||
|
||||
def getAutomationDefinition(self, automationId: str) -> Optional[AutomationDefinition]:
|
||||
"""Returns an automation definition by ID if user has access, with computed status."""
|
||||
def getAutomationDefinition(self, automationId: str, includeSystemFields: bool = False) -> Optional[AutomationDefinition]:
|
||||
"""Returns an automation definition by ID if user has access, with computed status.
|
||||
|
||||
Args:
|
||||
automationId: ID of the automation to get
|
||||
includeSystemFields: If True, returns raw dict with system fields (_createdBy, etc).
|
||||
If False (default), returns Pydantic model without system fields.
|
||||
"""
|
||||
try:
|
||||
# Use RBAC filtering
|
||||
filtered = getRecordsetWithRBAC(self.db,
|
||||
|
|
@ -1749,6 +1755,16 @@ class ChatObjects:
|
|||
automation["executionLogs"] = []
|
||||
# Enrich with user and mandate names
|
||||
self._enrichAutomationWithUserAndMandate(automation)
|
||||
|
||||
# For internal use (execution), return raw dict with system fields
|
||||
if includeSystemFields:
|
||||
# Return as simple namespace object so getattr works
|
||||
class AutomationWithSystemFields:
|
||||
def __init__(self, data):
|
||||
for key, value in data.items():
|
||||
setattr(self, key, value)
|
||||
return AutomationWithSystemFields(automation)
|
||||
|
||||
# Clean metadata fields and return Pydantic model
|
||||
cleanedRecord = {k: v for k, v in automation.items() if not k.startswith("_")}
|
||||
return AutomationDefinition(**cleanedRecord)
|
||||
|
|
@ -1771,9 +1787,12 @@ class ChatObjects:
|
|||
|
||||
# Ensure database connector has correct userId context
|
||||
# The connector should have been initialized with userId, but ensure it's updated
|
||||
if self.userId and hasattr(self.db, 'updateContext'):
|
||||
if not self.userId:
|
||||
logger.error(f"createAutomationDefinition: userId is not set! Cannot set _createdBy. currentUser={self.currentUser}")
|
||||
elif hasattr(self.db, 'updateContext'):
|
||||
try:
|
||||
self.db.updateContext(self.userId)
|
||||
logger.debug(f"createAutomationDefinition: Updated database context with userId={self.userId}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not update database context: {e}")
|
||||
|
||||
|
|
|
|||
|
|
@ -248,7 +248,10 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
|
|||
recordFilter={"userId": userId, "featureInstanceId": instanceId}
|
||||
)
|
||||
|
||||
logger.debug(f"_getInstancePermissions: userId={userId}, instanceId={instanceId}, featureAccesses={len(featureAccesses) if featureAccesses else 0}")
|
||||
|
||||
if not featureAccesses:
|
||||
logger.debug(f"_getInstancePermissions: No FeatureAccess found for user {userId} and instance {instanceId}")
|
||||
return permissions
|
||||
|
||||
# Get role IDs via FeatureAccessRole junction table
|
||||
|
|
@ -259,7 +262,10 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
|
|||
)
|
||||
roleIds = [far.get("roleId") for far in featureAccessRoles]
|
||||
|
||||
logger.debug(f"_getInstancePermissions: featureAccessId={featureAccessId}, roleIds={roleIds}")
|
||||
|
||||
if not roleIds:
|
||||
logger.debug(f"_getInstancePermissions: No roles found for FeatureAccess {featureAccessId}")
|
||||
return permissions
|
||||
|
||||
# Get permissions (AccessRules) for all roles
|
||||
|
|
@ -269,6 +275,8 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
|
|||
recordFilter={"roleId": roleId}
|
||||
)
|
||||
|
||||
logger.debug(f"_getInstancePermissions: roleId={roleId}, accessRules={len(accessRules) if accessRules else 0}")
|
||||
|
||||
for rule in accessRules:
|
||||
context = rule.get("context", "")
|
||||
item = rule.get("item", "")
|
||||
|
|
@ -303,8 +311,13 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
|
|||
|
||||
# Handle UI context (views)
|
||||
elif context == "UI" or context == AccessRuleContext.UI:
|
||||
ruleView = rule.get("view", False)
|
||||
if item:
|
||||
permissions["views"][item] = permissions["views"].get(item, False) or rule.get("view", False)
|
||||
# Specific view rule
|
||||
permissions["views"][item] = permissions["views"].get(item, False) or ruleView
|
||||
elif ruleView:
|
||||
# item=None means all views - set a wildcard flag
|
||||
permissions["views"]["_all"] = True
|
||||
|
||||
return permissions
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ Shared utilities for model attributes and labels.
|
|||
"""
|
||||
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
from pydantic_core import PydanticUndefined
|
||||
from typing import Dict, Any, List, Type, Optional, Union
|
||||
import inspect
|
||||
import importlib
|
||||
|
|
@ -212,7 +213,8 @@ def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguag
|
|||
# Don't call it here - it's meant to be called per-instance
|
||||
# Instead, store a marker that indicates it exists
|
||||
field_default = None # Frontend should use first option or specific logic
|
||||
elif default_value is not ...: # Ellipsis means no default
|
||||
elif default_value is not ... and default_value is not PydanticUndefined:
|
||||
# Ellipsis or PydanticUndefined means no default
|
||||
# Convert enum to its value if it's an enum
|
||||
if hasattr(default_value, 'value'):
|
||||
field_default = default_value.value
|
||||
|
|
|
|||
|
|
@ -76,8 +76,8 @@ async def executeAutomation(automationId: str, services) -> ChatWorkflow:
|
|||
}
|
||||
|
||||
try:
|
||||
# 1. Load automation definition
|
||||
automation = services.interfaceDbChat.getAutomationDefinition(automationId)
|
||||
# 1. Load automation definition (with system fields for _createdBy access)
|
||||
automation = services.interfaceDbChat.getAutomationDefinition(automationId, includeSystemFields=True)
|
||||
if not automation:
|
||||
raise ValueError(f"Automation {automationId} not found")
|
||||
|
||||
|
|
@ -105,18 +105,17 @@ async def executeAutomation(automationId: str, services) -> ChatWorkflow:
|
|||
# 3. Get user who created automation
|
||||
creatorUserId = getattr(automation, "_createdBy", None)
|
||||
|
||||
# CRITICAL: Automation MUST run as creator user only, or fail
|
||||
# _createdBy is a system attribute - must be present
|
||||
if not creatorUserId:
|
||||
errorMsg = f"Automation {automationId} has no creator user (_createdBy field missing). Cannot execute automation."
|
||||
logger.error(errorMsg)
|
||||
executionLog["messages"].append(errorMsg)
|
||||
raise ValueError(errorMsg)
|
||||
|
||||
# Get user from database using services
|
||||
# Get creator user from database
|
||||
creatorUser = services.interfaceDbApp.getUser(creatorUserId)
|
||||
if not creatorUser:
|
||||
raise ValueError(f"Creator user {creatorUserId} not found")
|
||||
|
||||
executionLog["messages"].append(f"Using creator user: {creatorUserId}")
|
||||
|
||||
# 4. Create UserInputRequest from plan
|
||||
|
|
@ -205,10 +204,17 @@ async def syncAutomationEvents(services, eventUser) -> Dict[str, Any]:
|
|||
registeredEvents = {}
|
||||
|
||||
for automation in filtered:
|
||||
automationId = automation.id
|
||||
isActive = automation.active if hasattr(automation, 'active') else False
|
||||
currentEventId = automation.eventId if hasattr(automation, 'eventId') else None
|
||||
schedule = automation.schedule if hasattr(automation, 'schedule') else None
|
||||
# Handle both dict and object access patterns
|
||||
if isinstance(automation, dict):
|
||||
automationId = automation.get('id')
|
||||
isActive = automation.get('active', False)
|
||||
currentEventId = automation.get('eventId')
|
||||
schedule = automation.get('schedule')
|
||||
else:
|
||||
automationId = automation.id
|
||||
isActive = automation.active if hasattr(automation, 'active') else False
|
||||
currentEventId = automation.eventId if hasattr(automation, 'eventId') else None
|
||||
schedule = automation.schedule if hasattr(automation, 'schedule') else None
|
||||
|
||||
if not schedule:
|
||||
logger.warning(f"Automation {automationId} has no schedule, skipping")
|
||||
|
|
@ -287,8 +293,8 @@ def createAutomationEventHandler(automationId: str, eventUser):
|
|||
# Get services for event user (provides access to interfaces)
|
||||
eventServices = getServices(eventUser, None)
|
||||
|
||||
# Load automation using event user context
|
||||
automation = eventServices.interfaceDbChat.getAutomationDefinition(automationId)
|
||||
# Load automation using event user context (with system fields for _createdBy access)
|
||||
automation = eventServices.interfaceDbChat.getAutomationDefinition(automationId, includeSystemFields=True)
|
||||
if not automation or not getattr(automation, "active", False):
|
||||
logger.warning(f"Automation {automationId} not found or not active, skipping execution")
|
||||
return
|
||||
|
|
|
|||
Loading…
Reference in a new issue