testing fixes, udb source handling fixes
This commit is contained in:
parent
e942770ffc
commit
19be818fbb
16 changed files with 179 additions and 13 deletions
|
|
@ -138,7 +138,7 @@ class BillingSettings(BaseModel):
|
|||
|
||||
warningThresholdPercent: float = Field(
|
||||
default=10.0,
|
||||
description="Warning threshold as percentage",
|
||||
description="Benachrichtigung wenn das AI-Guthaben unter diesen Prozentsatz des Gesamtbudgets fällt",
|
||||
json_schema_extra={"label": "Warnschwelle (%)"},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,22 @@ class FileFolder(PowerOnModel):
|
|||
"fk_target": {"db": "poweron_app", "table": "FeatureInstance"},
|
||||
},
|
||||
)
|
||||
scope: str = Field(
|
||||
default="personal",
|
||||
description="Data visibility scope: personal, featureInstance, mandate, global. Inherited by files in this folder.",
|
||||
json_schema_extra={
|
||||
"label": "Sichtbarkeit",
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": False,
|
||||
"frontend_options": [
|
||||
{"value": "personal", "label": "Persönlich"},
|
||||
{"value": "featureInstance", "label": "Feature-Instanz"},
|
||||
{"value": "mandate", "label": "Mandant"},
|
||||
{"value": "global", "label": "Global"},
|
||||
],
|
||||
},
|
||||
)
|
||||
neutralize: bool = Field(
|
||||
default=False,
|
||||
description="Whether files in this folder should be neutralized before AI processing. Inherited by new/moved files.",
|
||||
|
|
|
|||
|
|
@ -1510,12 +1510,16 @@ def _ensureUiContextRules(db: DatabaseConnector) -> None:
|
|||
if roleId and item:
|
||||
existingCombinations.add((roleId, item))
|
||||
|
||||
# Check each navigation item and add missing rules
|
||||
# Check each navigation item and add missing rules (including subgroup items)
|
||||
missingRules = []
|
||||
for section in NAVIGATION_SECTIONS:
|
||||
isAdminSection = section.get("adminOnly", False)
|
||||
|
||||
for item in section.get("items", []):
|
||||
allItems = list(section.get("items", []))
|
||||
for subgroup in section.get("subgroups", []):
|
||||
allItems.extend(subgroup.get("items", []))
|
||||
|
||||
for item in allItems:
|
||||
objectKey = item.get("objectKey")
|
||||
if not objectKey:
|
||||
continue
|
||||
|
|
@ -1865,6 +1869,7 @@ def _createStoreResourceRules(db: DatabaseConnector) -> None:
|
|||
"resource.store.teamsbot",
|
||||
"resource.store.workspace",
|
||||
"resource.store.commcoach",
|
||||
"resource.store.trustee",
|
||||
]
|
||||
|
||||
storeRules = []
|
||||
|
|
|
|||
|
|
@ -502,6 +502,45 @@ def move_folder(
|
|||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.patch("/folders/{folderId}/scope")
|
||||
@limiter.limit("10/minute")
|
||||
def _updateFolderScope(
|
||||
request: Request,
|
||||
folderId: str = Path(..., description="ID of the folder"),
|
||||
scope: str = Body(..., embed=True),
|
||||
context: RequestContext = Depends(getRequestContext),
|
||||
) -> Dict[str, Any]:
|
||||
"""Update the scope of a folder. Propagates to all files inside (recursively). Global scope requires sysAdmin."""
|
||||
validScopes = {"personal", "featureInstance", "mandate", "global"}
|
||||
if scope not in validScopes:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid scope: {scope}. Must be one of {validScopes}")
|
||||
if scope == "global" and not _hasSysAdminRole(context.user):
|
||||
raise HTTPException(status_code=403, detail=routeApiMsg("Only sysadmins can set global scope"))
|
||||
try:
|
||||
mgmt = interfaceDbManagement.getInterface(
|
||||
context.user,
|
||||
mandateId=str(context.mandateId) if context.mandateId else None,
|
||||
featureInstanceId=str(context.featureInstanceId) if context.featureInstanceId else None,
|
||||
)
|
||||
folder = mgmt.getFolder(folderId)
|
||||
if not folder:
|
||||
raise HTTPException(status_code=404, detail=routeApiMsg("Folder not found"))
|
||||
mgmt.updateFolder(folderId, {"scope": scope})
|
||||
fileIds = _collectFolderFileIds(mgmt, folderId)
|
||||
for fid in fileIds:
|
||||
try:
|
||||
mgmt.updateFile(fid, {"scope": scope})
|
||||
except Exception as e:
|
||||
logger.error("Folder scope propagation: failed to update file %s: %s", fid, e)
|
||||
logger.info("Updated scope=%s for folder %s: %d files affected", scope, folderId, len(fileIds))
|
||||
return {"folderId": folderId, "scope": scope, "filesUpdated": len(fileIds)}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating folder scope: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.patch("/folders/{folderId}/neutralize")
|
||||
@limiter.limit("10/minute")
|
||||
def updateFolderNeutralize(
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import json
|
|||
from pydantic import BaseModel, Field
|
||||
|
||||
# Import auth module
|
||||
from modules.auth import limiter, requireSysAdminRole, getRequestContext, RequestContext
|
||||
from modules.auth import limiter, requireSysAdminRole, getRequestContext, getCurrentUser, RequestContext
|
||||
|
||||
# Import interfaces
|
||||
import modules.interfaces.interfaceDbApp as interfaceDbApp
|
||||
|
|
@ -341,32 +341,54 @@ def create_mandate(
|
|||
detail=f"Failed to create mandate: {str(e)}"
|
||||
)
|
||||
|
||||
_MANDATE_ADMIN_EDITABLE_FIELDS = {"label"}
|
||||
|
||||
@router.put("/{mandateId}", response_model=Mandate)
|
||||
@limiter.limit("10/minute")
|
||||
def update_mandate(
|
||||
request: Request,
|
||||
mandateId: str = Path(..., description="ID of the mandate to update"),
|
||||
mandateData: dict = Body(..., description="Mandate update data"),
|
||||
currentUser: User = Depends(requireSysAdminRole)
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> Mandate:
|
||||
"""
|
||||
Update an existing mandate.
|
||||
MULTI-TENANT: SysAdmin-only.
|
||||
MULTI-TENANT:
|
||||
- SysAdmin: full update
|
||||
- MandateAdmin: only label
|
||||
"""
|
||||
from modules.auth import _hasSysAdminRole as _checkSysAdminRole
|
||||
isSysAdmin = _checkSysAdminRole(str(currentUser.id))
|
||||
|
||||
if not isSysAdmin:
|
||||
context = getRequestContext(request, currentUser=currentUser)
|
||||
isMandateAdmin = _hasMandateAdminRole(context, mandateId)
|
||||
if not isMandateAdmin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=routeApiMsg("Admin role required to update mandate")
|
||||
)
|
||||
|
||||
try:
|
||||
logger.debug(f"Updating mandate {mandateId} with data: {mandateData}")
|
||||
|
||||
appInterface = interfaceDbApp.getRootInterface()
|
||||
|
||||
# Check if mandate exists
|
||||
existingMandate = appInterface.getMandate(mandateId)
|
||||
if not existingMandate:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Mandate with ID {mandateId} not found"
|
||||
)
|
||||
|
||||
if not isSysAdmin:
|
||||
mandateData = {k: v for k, v in mandateData.items() if k in _MANDATE_ADMIN_EDITABLE_FIELDS}
|
||||
if not mandateData:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=routeApiMsg("No editable fields submitted")
|
||||
)
|
||||
|
||||
# Update mandate - mandateData is already a dict
|
||||
updatedMandate = appInterface.updateMandate(mandateId, mandateData)
|
||||
|
||||
if not updatedMandate:
|
||||
|
|
@ -375,7 +397,7 @@ def update_mandate(
|
|||
detail=routeApiMsg("Failed to update mandate")
|
||||
)
|
||||
|
||||
logger.info(f"Mandate {mandateId} updated by SysAdmin {currentUser.id}")
|
||||
logger.info(f"Mandate {mandateId} updated by user {currentUser.id} (sysadmin={isSysAdmin})")
|
||||
|
||||
return updatedMandate
|
||||
except HTTPException:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"""PATCH endpoints for DataSource and FeatureDataSource scope/neutralize tagging."""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, Path, Request, Body
|
||||
from modules.auth import limiter, getRequestContext, RequestContext
|
||||
|
|
@ -97,3 +97,32 @@ def _updateDataSourceNeutralize(
|
|||
except Exception as e:
|
||||
logger.error("Error updating datasource neutralize: %s", e)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.patch("/{sourceId}/neutralize-fields")
|
||||
@limiter.limit("30/minute")
|
||||
def _updateNeutralizeFields(
|
||||
request: Request,
|
||||
sourceId: str = Path(..., description="ID of the FeatureDataSource"),
|
||||
neutralizeFields: List[str] = Body(..., embed=True),
|
||||
context: RequestContext = Depends(getRequestContext),
|
||||
) -> Dict[str, Any]:
|
||||
"""Update the list of field names to neutralize on a FeatureDataSource."""
|
||||
try:
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
rootIf = getRootInterface()
|
||||
rec = rootIf.db.getRecord(FeatureDataSource, sourceId)
|
||||
if not rec:
|
||||
raise HTTPException(status_code=404, detail=f"FeatureDataSource {sourceId} not found")
|
||||
|
||||
cleanFields = [f for f in neutralizeFields if f and isinstance(f, str)] if neutralizeFields else []
|
||||
rootIf.db.recordModify(FeatureDataSource, sourceId, {
|
||||
"neutralizeFields": cleanFields if cleanFields else None,
|
||||
})
|
||||
logger.info("Updated neutralizeFields=%s for FeatureDataSource %s", cleanFields, sourceId)
|
||||
return {"sourceId": sourceId, "neutralizeFields": cleanFields, "updated": True}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Error updating neutralizeFields: %s", e)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
|
|
|||
|
|
@ -481,8 +481,9 @@ def update_user(
|
|||
detail=f"User with ID {userId} not found"
|
||||
)
|
||||
|
||||
# Update user
|
||||
updatedUser = rootInterface.updateUser(userId, userData)
|
||||
# SysAdmins may toggle the isSysAdmin flag on other users
|
||||
callerIsSysAdmin = context.isSysAdmin or context.hasSysAdminRole
|
||||
updatedUser = rootInterface.updateUser(userId, userData, allowSysAdminChange=(callerIsSysAdmin and not isSelfUpdate))
|
||||
|
||||
if not updatedUser:
|
||||
raise HTTPException(
|
||||
|
|
|
|||
|
|
@ -505,6 +505,11 @@ RESOURCE_OBJECTS = [
|
|||
"label": "Store: CommCoach",
|
||||
"meta": {"category": "store", "featureCode": "commcoach"}
|
||||
},
|
||||
{
|
||||
"objectKey": "resource.store.trustee",
|
||||
"label": "Store: Trustee",
|
||||
"meta": {"category": "store", "featureCode": "trustee"}
|
||||
},
|
||||
{
|
||||
"objectKey": "resource.system.api.auth",
|
||||
"label": "Authentifizierungs-API",
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ from modules.workflows.automation2.executors import (
|
|||
)
|
||||
from modules.features.graphicalEditor.portTypes import _normalizeToSchema
|
||||
from modules.features.graphicalEditor.nodeDefinitions import STATIC_NODE_TYPES
|
||||
from modules.serviceCenter.services.serviceSubscription.mainServiceSubscription import SubscriptionInactiveException as _SubscriptionInactiveException
|
||||
from modules.serviceCenter.services.serviceBilling.mainServiceBilling import BillingContextError as _BillingContextError
|
||||
from modules.workflows.automation2.runEnvelope import normalize_run_envelope
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -279,7 +281,7 @@ async def _executeWithRetry(executor, node, context, maxRetries: int = 0, retryD
|
|||
try:
|
||||
result = await executor.execute(node, context)
|
||||
return result, attempt
|
||||
except (PauseForHumanTaskError, PauseForEmailWaitError):
|
||||
except (PauseForHumanTaskError, PauseForEmailWaitError, _SubscriptionInactiveException, _BillingContextError):
|
||||
raise
|
||||
except Exception as e:
|
||||
lastError = e
|
||||
|
|
@ -488,6 +490,10 @@ async def executeGraph(
|
|||
_updateStepLog(automation2_interface, _rStepId, "completed",
|
||||
durationMs=int((time.time() - _rStepStart) * 1000))
|
||||
raise
|
||||
except (_SubscriptionInactiveException, _BillingContextError):
|
||||
_updateStepLog(automation2_interface, _rStepId, "failed",
|
||||
error="Subscription/Billing error", durationMs=int((time.time() - _rStepStart) * 1000))
|
||||
raise
|
||||
except Exception as ex:
|
||||
_updateStepLog(automation2_interface, _rStepId, "failed",
|
||||
error=str(ex), durationMs=int((time.time() - _rStepStart) * 1000))
|
||||
|
|
@ -625,6 +631,11 @@ async def executeGraph(
|
|||
_updateStepLog(automation2_interface, _bStepId, "completed",
|
||||
durationMs=int((time.time() - _bStepStart) * 1000))
|
||||
raise
|
||||
except (_SubscriptionInactiveException, _BillingContextError):
|
||||
if _bStepId:
|
||||
_updateStepLog(automation2_interface, _bStepId, "failed",
|
||||
error="Subscription/Billing error", durationMs=int((time.time() - _bStepStart) * 1000))
|
||||
raise
|
||||
except Exception as ex:
|
||||
if _bStepId:
|
||||
_updateStepLog(automation2_interface, _bStepId, "failed",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ from modules.features.graphicalEditor.portTypes import (
|
|||
_normalizeError,
|
||||
_unwrapTransit,
|
||||
)
|
||||
from modules.serviceCenter.services.serviceSubscription.mainServiceSubscription import SubscriptionInactiveException as _SubscriptionInactiveException
|
||||
from modules.serviceCenter.services.serviceBilling.mainServiceBilling import BillingContextError as _BillingContextError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -299,6 +301,8 @@ class ActionNodeExecutor:
|
|||
try:
|
||||
executor = ActionExecutor(self.services)
|
||||
result = await executor.executeAction(methodName, actionName, resolvedParams)
|
||||
except (_SubscriptionInactiveException, _BillingContextError):
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception("ActionNodeExecutor node %s FAILED: %s", nodeId, e)
|
||||
return _normalizeError(e, outputSchema)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ from typing import Any, Dict, List
|
|||
|
||||
from modules.datamodels.datamodelAi import AiCallOptions, AiCallRequest, OperationTypeEnum
|
||||
from modules.datamodels.datamodelChat import ActionResult
|
||||
from modules.serviceCenter.services.serviceSubscription.mainServiceSubscription import SubscriptionInactiveException
|
||||
from modules.serviceCenter.services.serviceBilling.mainServiceBilling import BillingContextError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -66,6 +68,8 @@ async def consolidate(self, parameters: Dict[str, Any]) -> ActionResult:
|
|||
options=AiCallOptions(operationType=OperationTypeEnum.DATA_ANALYSE),
|
||||
)
|
||||
resp = await ai_service.callAi(req)
|
||||
except (SubscriptionInactiveException, BillingContextError):
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception("consolidate: AI call failed: %s", e)
|
||||
return ActionResult.isFailure(error=str(e))
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
|||
from modules.datamodels.datamodelExtraction import ContentPart
|
||||
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum
|
||||
from modules.datamodels.datamodelWorkflow import AiResponse, DocumentData
|
||||
from modules.serviceCenter.services.serviceSubscription.mainServiceSubscription import SubscriptionInactiveException
|
||||
from modules.serviceCenter.services.serviceBilling.mainServiceBilling import BillingContextError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -125,6 +127,8 @@ async def generateCode(self, parameters: Dict[str, Any]) -> ActionResult:
|
|||
|
||||
return ActionResult.isSuccess(documents=documents)
|
||||
|
||||
except (SubscriptionInactiveException, BillingContextError):
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error in code generation: {str(e)}")
|
||||
return ActionResult.isFailure(error=str(e))
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
|||
from modules.datamodels.datamodelExtraction import ContentPart
|
||||
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum
|
||||
from modules.datamodels.datamodelWorkflow import AiResponse, DocumentData
|
||||
from modules.serviceCenter.services.serviceSubscription.mainServiceSubscription import SubscriptionInactiveException
|
||||
from modules.serviceCenter.services.serviceBilling.mainServiceBilling import BillingContextError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -127,6 +129,8 @@ async def generateDocument(self, parameters: Dict[str, Any]) -> ActionResult:
|
|||
|
||||
return ActionResult.isSuccess(documents=documents)
|
||||
|
||||
except (SubscriptionInactiveException, BillingContextError):
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error in document generation: {str(e)}")
|
||||
return ActionResult.isFailure(error=str(e))
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ from typing import Dict, Any, List, Optional
|
|||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, ProcessingModeEnum
|
||||
from modules.datamodels.datamodelExtraction import ContentPart
|
||||
from modules.serviceCenter.services.serviceSubscription.mainServiceSubscription import SubscriptionInactiveException
|
||||
from modules.serviceCenter.services.serviceBilling.mainServiceBilling import BillingContextError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -340,6 +342,13 @@ async def process(self, parameters: Dict[str, Any]) -> ActionResult:
|
|||
|
||||
return ActionResult.isSuccess(documents=final_documents)
|
||||
|
||||
except (SubscriptionInactiveException, BillingContextError):
|
||||
try:
|
||||
if operationId:
|
||||
self.services.chat.progressLogFinish(operationId, False)
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error in AI processing: {str(e)}")
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import json
|
|||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.serviceCenter import ServiceCenterContext, getService, can_access_service
|
||||
from modules.serviceCenter.services.serviceSubscription.mainServiceSubscription import SubscriptionInactiveException
|
||||
from modules.serviceCenter.services.serviceBilling.mainServiceBilling import BillingContextError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -112,6 +114,13 @@ async def webResearch(self, parameters: Dict[str, Any]) -> ActionResult:
|
|||
|
||||
return ActionResult.isSuccess(documents=[actionDocument])
|
||||
|
||||
except (SubscriptionInactiveException, BillingContextError):
|
||||
try:
|
||||
if operationId:
|
||||
self.services.chat.progressLogFinish(operationId, False)
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error in web research: {str(e)}")
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ from typing import Dict, Any, List, Optional, Tuple
|
|||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument, ChatDocument, ChatMessage
|
||||
from modules.datamodels.datamodelDocref import DocumentReferenceList, DocumentItemReference
|
||||
from modules.datamodels.datamodelAi import AiCallOptions, AiCallRequest, OperationTypeEnum
|
||||
from modules.serviceCenter.services.serviceSubscription.mainServiceSubscription import SubscriptionInactiveException as _SubscriptionInactiveException
|
||||
from modules.serviceCenter.services.serviceBilling.mainServiceBilling import BillingContextError as _BillingContextError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -410,6 +412,8 @@ async def _extractOne(
|
|||
documentData=json.dumps(out),
|
||||
mimeType="application/json",
|
||||
)
|
||||
except (_SubscriptionInactiveException, _BillingContextError):
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f"Extract failed for {f.get('fileName')}")
|
||||
return ActionDocument(
|
||||
|
|
|
|||
Loading…
Reference in a new issue