fixed language logic items
This commit is contained in:
parent
0f5d695960
commit
4dfc0afd06
37 changed files with 978 additions and 277 deletions
|
|
@ -2,6 +2,7 @@
|
|||
# All rights reserved.
|
||||
"""Utility datamodels: Prompt, TextMultilingual."""
|
||||
|
||||
import re as _re
|
||||
from typing import Any, Dict, Optional
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from modules.datamodels.datamodelBase import PowerOnModel
|
||||
|
|
@ -39,68 +40,95 @@ class Prompt(PowerOnModel):
|
|||
@field_validator('isSystem', mode='before')
|
||||
@classmethod
|
||||
def _coerceIsSystem(cls, v):
|
||||
"""Existing records may have isSystem=None (field didn't exist). Treat None as False."""
|
||||
if v is None:
|
||||
return False
|
||||
return v
|
||||
|
||||
|
||||
class TextMultilingual(BaseModel):
|
||||
"""Multilingual text field. Language codes follow ISO 639-1 (en, de, fr, it, …)."""
|
||||
en: str = Field(description="English text (default language, required)")
|
||||
de: Optional[str] = Field(None, description="German text")
|
||||
fr: Optional[str] = Field(None, description="French text")
|
||||
it: Optional[str] = Field(None, description="Italian text")
|
||||
"""Multilingual text field stored as JSONB: {"xx": "source text", "de": "...", "en": "...", ...}.
|
||||
|
||||
@field_validator('en')
|
||||
- xx = source/default text (required). Same role as xx in the UI i18n system.
|
||||
- All language codes (de, en, fr, ...) are dynamic, populated via batch translation.
|
||||
- No hardcoded language fields. The DB column is JSONB with arbitrary keys.
|
||||
"""
|
||||
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
xx: str = Field(description="Source/default text (required)")
|
||||
|
||||
@field_validator('xx')
|
||||
@classmethod
|
||||
def _validateEnRequired(cls, v):
|
||||
def _validateXxRequired(cls, v):
|
||||
if not v or not v.strip():
|
||||
raise ValueError("English text (en) is required and cannot be empty")
|
||||
raise ValueError("Source text (xx) is required and cannot be empty")
|
||||
return v
|
||||
|
||||
def model_dump(self, **kwargs) -> Dict[str, str]:
|
||||
result = {}
|
||||
for key in self.model_fields:
|
||||
value = getattr(self, key, None)
|
||||
if value is not None:
|
||||
result[key] = value
|
||||
result = {"xx": self.xx}
|
||||
if self.__pydantic_extra__:
|
||||
for k, v in self.__pydantic_extra__.items():
|
||||
if v is not None and isinstance(v, str):
|
||||
result[k] = v
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, str]) -> 'TextMultilingual':
|
||||
fields = {k: data[k] for k in cls.model_fields if k in data}
|
||||
fields.setdefault('en', '')
|
||||
return cls(**fields)
|
||||
cleaned = {k: v for k, v in data.items() if v is not None and isinstance(v, str)}
|
||||
if not cleaned.get('xx'):
|
||||
cleaned['xx'] = cleaned.get('de') or next((v for v in cleaned.values() if v), '—')
|
||||
return cls(**cleaned)
|
||||
|
||||
def get_text(self, lang: str = 'en') -> str:
|
||||
"""Get text for *lang*. Falls back to English."""
|
||||
value = getattr(self, lang, None)
|
||||
if value:
|
||||
def get_text(self, lang: str = 'de') -> str:
|
||||
"""Get text for a language. Falls back to xx (source text)."""
|
||||
if lang == 'xx':
|
||||
return self.xx
|
||||
extra = self.__pydantic_extra__ or {}
|
||||
value = extra.get(lang)
|
||||
if value and isinstance(value, str):
|
||||
return value
|
||||
return self.en
|
||||
return self.xx
|
||||
|
||||
@classmethod
|
||||
def fromUniform(cls, text: str) -> "TextMultilingual":
|
||||
"""Same string in all languages (bootstrap / i18n key until per-language values exist in DB)."""
|
||||
"""Create with source text only. Languages are populated by batch translation."""
|
||||
t = text.strip()
|
||||
if not t:
|
||||
raise ValueError("Text must be non-empty")
|
||||
return cls(en=t, de=t, fr=t, it=t)
|
||||
return cls(xx=t)
|
||||
|
||||
|
||||
_REPR_PATTERN = _re.compile(r"(\w+)='([^']*)'")
|
||||
|
||||
|
||||
def _parseReprString(s: str) -> Optional[Dict[str, str]]:
|
||||
"""Parse a Pydantic repr string like "en='text' de=None fr=" into a dict."""
|
||||
matches = _REPR_PATTERN.findall(s)
|
||||
if not matches:
|
||||
return None
|
||||
result = {}
|
||||
for code, text in matches:
|
||||
if len(code) <= 5 and text:
|
||||
result[code] = text
|
||||
return result if result else None
|
||||
|
||||
|
||||
def coerce_text_multilingual(val: Any) -> TextMultilingual:
|
||||
"""Normalize str, dict, or TextMultilingual for Role.description and similar fields."""
|
||||
"""Normalize str, dict, or TextMultilingual into a valid TextMultilingual instance."""
|
||||
if isinstance(val, TextMultilingual):
|
||||
return val
|
||||
if isinstance(val, dict):
|
||||
if not val:
|
||||
return TextMultilingual.fromUniform("—")
|
||||
d = {k: val[k] for k in TextMultilingual.model_fields if k in val and val[k] is not None}
|
||||
if not d.get("en"):
|
||||
d["en"] = (d.get("de") or d.get("fr") or "—").strip() or "—"
|
||||
return TextMultilingual(**{k: d[k] for k in TextMultilingual.model_fields if k in d})
|
||||
cleaned = {k: v for k, v in val.items() if v is not None and isinstance(v, str)}
|
||||
if not cleaned.get("xx"):
|
||||
cleaned["xx"] = cleaned.get("de") or next((v for v in cleaned.values() if v), "—")
|
||||
return TextMultilingual(**cleaned)
|
||||
if isinstance(val, str) and val.strip():
|
||||
parsed = _parseReprString(val)
|
||||
if parsed:
|
||||
if not parsed.get("xx"):
|
||||
parsed["xx"] = parsed.get("de") or next((v for v in parsed.values() if v), val.strip())
|
||||
return TextMultilingual(**parsed)
|
||||
return TextMultilingual.fromUniform(val)
|
||||
return TextMultilingual.fromUniform("—")
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from modules.datamodels.datamodelUam import User
|
|||
from modules.connectors.connectorDbPostgre import DatabaseConnector
|
||||
from modules.shared.timeUtils import getIsoTimestamp
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
from modules.shared.i18nRegistry import t
|
||||
|
||||
from .datamodelCommcoach import (
|
||||
CoachingContext, CoachingContextStatus,
|
||||
|
|
@ -412,9 +413,17 @@ def _calcGoalProgress(goalsRaw) -> Optional[int]:
|
|||
return round(done / len(goals) * 100)
|
||||
|
||||
|
||||
_LEVELS = [
|
||||
(50, 5, "master", t("Meister")),
|
||||
(25, 4, "expert", t("Experte")),
|
||||
(10, 3, "advanced", t("Fortgeschritten")),
|
||||
(3, 2, "engaged", t("Engagiert")),
|
||||
]
|
||||
t("Einsteiger")
|
||||
|
||||
|
||||
def _calcLevel(totalSessions: int) -> Dict[str, Any]:
|
||||
levels = [(50, 5, "Meister"), (25, 4, "Experte"), (10, 3, "Fortgeschritten"), (3, 2, "Engagiert")]
|
||||
for threshold, number, label in levels:
|
||||
for threshold, number, code, _label in _LEVELS:
|
||||
if totalSessions >= threshold:
|
||||
return {"number": number, "label": label, "totalSessions": totalSessions}
|
||||
return {"number": 1, "label": "Einsteiger", "totalSessions": totalSessions}
|
||||
return {"number": number, "code": code, "label": t(_label), "totalSessions": totalSessions}
|
||||
return {"number": 1, "code": "beginner", "label": t("Einsteiger"), "totalSessions": totalSessions}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ UI_OBJECTS = [
|
|||
},
|
||||
{
|
||||
"objectKey": "ui.feature.commcoach.coaching",
|
||||
"label": "Coaching & Dossier",
|
||||
"label": "Arbeitsthemen",
|
||||
"meta": {"area": "coaching"}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ Checks and awards badges after each session completion.
|
|||
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional
|
||||
from modules.shared.i18nRegistry import t
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -78,6 +79,11 @@ BADGE_DEFINITIONS: Dict[str, Dict[str, Any]] = {
|
|||
},
|
||||
}
|
||||
|
||||
# Register all badge labels/descriptions at import time for i18n xx base set
|
||||
for _bd in BADGE_DEFINITIONS.values():
|
||||
t(_bd["label"])
|
||||
t(_bd["description"])
|
||||
|
||||
|
||||
async def checkAndAwardBadges(interface, userId: str, mandateId: str, instanceId: str,
|
||||
session: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
|
||||
|
|
@ -135,8 +141,8 @@ async def checkAndAwardBadges(interface, userId: str, mandateId: str, instanceId
|
|||
}
|
||||
newBadge = interface.awardBadge(badgeData)
|
||||
definition = BADGE_DEFINITIONS.get(badgeKey, {})
|
||||
newBadge["label"] = definition.get("label", badgeKey)
|
||||
newBadge["description"] = definition.get("description", "")
|
||||
newBadge["label"] = t(definition.get("label", badgeKey))
|
||||
newBadge["description"] = t(definition.get("description", ""))
|
||||
newBadge["icon"] = definition.get("icon", "star")
|
||||
awarded.append(newBadge)
|
||||
logger.info(f"Badge '{badgeKey}' awarded to user {userId}")
|
||||
|
|
@ -145,5 +151,8 @@ async def checkAndAwardBadges(interface, userId: str, mandateId: str, instanceId
|
|||
|
||||
|
||||
def getBadgeDefinitions() -> Dict[str, Dict[str, Any]]:
|
||||
"""Return all badge definitions for the frontend."""
|
||||
return BADGE_DEFINITIONS
|
||||
"""Return all badge definitions for the frontend (labels resolved via i18n)."""
|
||||
resolved = {}
|
||||
for key, defn in BADGE_DEFINITIONS.items():
|
||||
resolved[key] = {**defn, "label": t(defn["label"]), "description": t(defn["description"])}
|
||||
return resolved
|
||||
|
|
|
|||
|
|
@ -36,9 +36,10 @@ def default_manual_entry_point() -> Dict[str, Any]:
|
|||
}
|
||||
|
||||
|
||||
def _normalize_title(title: Any) -> str:
|
||||
def _normalize_title(title: Any, preferredLang: Optional[str] = None) -> str:
|
||||
if isinstance(title, dict):
|
||||
return str(title.get("de") or title.get("en") or title.get("fr") or "").strip()
|
||||
picked = (preferredLang and title.get(preferredLang)) or title.get("xx") or next(iter(title.values()), "")
|
||||
return str(picked).strip() if picked is not None else ""
|
||||
if isinstance(title, str) and title.strip():
|
||||
return title.strip()
|
||||
return "Start"
|
||||
|
|
|
|||
|
|
@ -481,7 +481,7 @@ def _deriveFormPayloadSchema(node: Dict[str, Any]) -> Optional[PortSchema]:
|
|||
if isinstance(f, dict) and f.get("name"):
|
||||
_lab = f.get("label")
|
||||
_desc = (
|
||||
str(_lab.get("de") or _lab.get("en") or f["name"])
|
||||
str(_lab.get("xx") or next(iter(_lab.values()), "") or f["name"])
|
||||
if isinstance(_lab, dict)
|
||||
else str(_lab if _lab is not None else f["name"])
|
||||
)
|
||||
|
|
|
|||
|
|
@ -32,10 +32,20 @@ routeApiMsg = apiRouteContext("routeFeatureGraphicalEditor")
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _pickInvocationTitleLabel(title: Any, requestLang: Optional[str]) -> str:
|
||||
if isinstance(title, str):
|
||||
return title
|
||||
if isinstance(title, dict) and title:
|
||||
picked = (requestLang and title.get(requestLang)) or title.get("xx") or next(iter(title.values()), "")
|
||||
return str(picked) if picked else ""
|
||||
return ""
|
||||
|
||||
|
||||
def _build_execute_run_envelope(
|
||||
body: Dict[str, Any],
|
||||
workflow: Optional[Dict[str, Any]],
|
||||
user_id: Optional[str],
|
||||
requestLang: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Build normalized run envelope from POST /execute body."""
|
||||
if isinstance(body.get("runEnvelope"), dict):
|
||||
|
|
@ -70,11 +80,7 @@ def _build_execute_run_envelope(
|
|||
}
|
||||
trig = trig_map.get(kind, "manual")
|
||||
title = inv.get("title") or {}
|
||||
label = ""
|
||||
if isinstance(title, dict):
|
||||
label = title.get("en") or title.get("de") or ""
|
||||
elif isinstance(title, str):
|
||||
label = title
|
||||
label = _pickInvocationTitleLabel(title, requestLang)
|
||||
base = default_run_envelope(
|
||||
trig,
|
||||
entry_point_id=inv.get("id"),
|
||||
|
|
@ -222,7 +228,12 @@ async def post_execute(
|
|||
workflowId,
|
||||
mandateId,
|
||||
)
|
||||
run_env = _build_execute_run_envelope(body, workflow_for_envelope, userId)
|
||||
run_env = _build_execute_run_envelope(
|
||||
body,
|
||||
workflow_for_envelope,
|
||||
userId,
|
||||
getattr(context.user, "language", None) if context.user else None,
|
||||
)
|
||||
|
||||
ge_interface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
||||
result = await executeGraph(
|
||||
|
|
@ -1041,11 +1052,10 @@ async def post_workflow_webhook(
|
|||
discoverMethods(services)
|
||||
|
||||
title = inv.get("title") or {}
|
||||
label = ""
|
||||
if isinstance(title, dict):
|
||||
label = title.get("en") or title.get("de") or ""
|
||||
elif isinstance(title, str):
|
||||
label = title
|
||||
label = _pickInvocationTitleLabel(
|
||||
title,
|
||||
getattr(context.user, "language", None) if context.user else None,
|
||||
)
|
||||
pl = body if isinstance(body, dict) else {}
|
||||
base = default_run_envelope(
|
||||
"webhook",
|
||||
|
|
@ -1103,11 +1113,10 @@ async def post_workflow_form_submit(
|
|||
discoverMethods(services)
|
||||
|
||||
title = inv.get("title") or {}
|
||||
label = ""
|
||||
if isinstance(title, dict):
|
||||
label = title.get("en") or title.get("de") or ""
|
||||
elif isinstance(title, str):
|
||||
label = title
|
||||
label = _pickInvocationTitleLabel(
|
||||
title,
|
||||
getattr(context.user, "language", None) if context.user else None,
|
||||
)
|
||||
pl = body if isinstance(body, dict) else {}
|
||||
base = default_run_envelope(
|
||||
"form",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Optional, Dict, Any
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class AccountingBookingLine(BaseModel):
|
||||
|
|
@ -51,7 +51,7 @@ class SyncResult(BaseModel):
|
|||
class ConnectorConfigField(BaseModel):
|
||||
"""Describes a configuration field required by a connector."""
|
||||
key: str
|
||||
label: Dict[str, str]
|
||||
label: str
|
||||
fieldType: str = "text"
|
||||
secret: bool = False
|
||||
required: bool = True
|
||||
|
|
@ -70,8 +70,8 @@ class BaseAccountingConnector(ABC):
|
|||
"""Unique type identifier, e.g. 'rma', 'bexio', 'abacus'."""
|
||||
|
||||
@abstractmethod
|
||||
def getConnectorLabel(self) -> Dict[str, str]:
|
||||
"""I18n display label."""
|
||||
def getConnectorLabel(self) -> str:
|
||||
"""German plaintext label (used as i18n key)."""
|
||||
|
||||
@abstractmethod
|
||||
def getRequiredConfigFields(self) -> List[ConnectorConfigField]:
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import os
|
|||
from typing import Dict, List, Optional
|
||||
|
||||
from .accountingConnectorBase import BaseAccountingConnector
|
||||
from modules.shared.i18nRegistry import t
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -58,10 +59,15 @@ class AccountingRegistry:
|
|||
self.discoverConnectors()
|
||||
result = []
|
||||
for connectorType, connector in self._connectors.items():
|
||||
fields = []
|
||||
for f in connector.getRequiredConfigFields():
|
||||
fd = f.model_dump()
|
||||
fd["label"] = t(f.label)
|
||||
fields.append(fd)
|
||||
result.append({
|
||||
"connectorType": connectorType,
|
||||
"label": connector.getConnectorLabel(),
|
||||
"configFields": [f.model_dump() for f in connector.getRequiredConfigFields()],
|
||||
"label": t(connector.getConnectorLabel()),
|
||||
"configFields": fields,
|
||||
})
|
||||
return result
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class AccountingConnectorAbacus(BaseAccountingConnector):
|
|||
def getConnectorType(self) -> str:
|
||||
return "abacus"
|
||||
|
||||
def getConnectorLabel(self) -> Dict[str, str]:
|
||||
def getConnectorLabel(self) -> str:
|
||||
return "Abacus ERP"
|
||||
|
||||
def getRequiredConfigFields(self) -> List[ConnectorConfigField]:
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class AccountingConnectorBexio(BaseAccountingConnector):
|
|||
def getConnectorType(self) -> str:
|
||||
return "bexio"
|
||||
|
||||
def getConnectorLabel(self) -> Dict[str, str]:
|
||||
def getConnectorLabel(self) -> str:
|
||||
return "Bexio"
|
||||
|
||||
def getRequiredConfigFields(self) -> List[ConnectorConfigField]:
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class AccountingConnectorRma(BaseAccountingConnector):
|
|||
def getConnectorType(self) -> str:
|
||||
return "rma"
|
||||
|
||||
def getConnectorLabel(self) -> Dict[str, str]:
|
||||
def getConnectorLabel(self) -> str:
|
||||
return "Run My Accounts"
|
||||
|
||||
def getRequiredConfigFields(self) -> List[ConnectorConfigField]:
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ def getQuickActions(
|
|||
if isinstance(multilingual, str):
|
||||
return multilingual
|
||||
if isinstance(multilingual, dict):
|
||||
return multilingual.get(lang) or multilingual.get("en") or multilingual.get("de") or next(iter(multilingual.values()), "")
|
||||
return multilingual.get(lang) or multilingual.get("xx") or next(iter(multilingual.values()), "")
|
||||
return ""
|
||||
|
||||
filteredActions = []
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ from modules.interfaces.interfaceAiObjects import AiObjects
|
|||
from modules.serviceCenter.core.serviceStreaming import get_event_manager
|
||||
from modules.serviceCenter.services.serviceAgent.datamodelAgent import AgentEventTypeEnum, PendingFileEdit
|
||||
from modules.shared.timeUtils import parseTimestamp
|
||||
from modules.shared.i18nRegistry import apiRouteContext
|
||||
from modules.shared.i18nRegistry import apiRouteContext, t
|
||||
routeApiMsg = apiRouteContext("routeFeatureWorkspace")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -135,10 +135,11 @@ def _validateInstanceAccess(instanceId: str, context: RequestContext):
|
|||
return mandateId, instanceConfig
|
||||
|
||||
|
||||
def _getChatInterface(context: RequestContext, featureInstanceId: str = None):
|
||||
def _getChatInterface(context: RequestContext, featureInstanceId: str = None, mandateId: str = None):
|
||||
effectiveMandateId = mandateId or (str(context.mandateId) if context.mandateId else None)
|
||||
return interfaceDbChat.getInterface(
|
||||
context.user,
|
||||
mandateId=str(context.mandateId) if context.mandateId else None,
|
||||
mandateId=effectiveMandateId,
|
||||
featureInstanceId=featureInstanceId,
|
||||
)
|
||||
|
||||
|
|
@ -543,7 +544,7 @@ async def streamWorkspaceStart(
|
|||
):
|
||||
"""Start or continue a Workspace session with SSE streaming via serviceAgent."""
|
||||
mandateId, instanceConfig = _validateInstanceAccess(instanceId, context)
|
||||
chatInterface = _getChatInterface(context, featureInstanceId=instanceId)
|
||||
chatInterface = _getChatInterface(context, featureInstanceId=instanceId, mandateId=mandateId)
|
||||
aiObjects = await _getAiObjects()
|
||||
eventManager = get_event_manager()
|
||||
|
||||
|
|
@ -907,7 +908,7 @@ async def stopWorkspace(
|
|||
workflowId: str = Path(...),
|
||||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
_mandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
queueId = f"workspace-{workflowId}"
|
||||
eventManager = get_event_manager()
|
||||
cancelled = await eventManager.cancel_agent(queueId)
|
||||
|
|
@ -933,8 +934,8 @@ async def listWorkspaceWorkflows(
|
|||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
"""List workspace workflows/conversations for this instance."""
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
chatInterface = _getChatInterface(context, featureInstanceId=instanceId)
|
||||
_mandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
chatInterface = _getChatInterface(context, featureInstanceId=instanceId, mandateId=_mandateId)
|
||||
workflows = chatInterface.getWorkflows() or []
|
||||
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
|
|
@ -1007,8 +1008,8 @@ async def resolveRag(
|
|||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
"""Build a RAG summary for a chat (workflow) to inject into the input area."""
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
chatInterface = _getChatInterface(context, featureInstanceId=instanceId)
|
||||
_mandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
chatInterface = _getChatInterface(context, featureInstanceId=instanceId, mandateId=_mandateId)
|
||||
messages = chatInterface.getMessages(body.chatId) or []
|
||||
|
||||
texts = []
|
||||
|
|
@ -1037,8 +1038,8 @@ async def patchWorkspaceWorkflow(
|
|||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
"""Update a workspace workflow (e.g. rename)."""
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
chatInterface = _getChatInterface(context, featureInstanceId=instanceId)
|
||||
_mandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
chatInterface = _getChatInterface(context, featureInstanceId=instanceId, mandateId=_mandateId)
|
||||
workflow = chatInterface.getWorkflow(workflowId)
|
||||
if not workflow:
|
||||
raise HTTPException(status_code=404, detail=f"Workflow {workflowId} not found")
|
||||
|
|
@ -1071,8 +1072,8 @@ async def deleteWorkspaceWorkflow(
|
|||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
"""Delete a workspace workflow and its messages."""
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
chatInterface = _getChatInterface(context, featureInstanceId=instanceId)
|
||||
_mandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
chatInterface = _getChatInterface(context, featureInstanceId=instanceId, mandateId=_mandateId)
|
||||
workflow = chatInterface.getWorkflow(workflowId)
|
||||
if not workflow:
|
||||
raise HTTPException(status_code=404, detail=f"Workflow {workflowId} not found")
|
||||
|
|
@ -1089,8 +1090,8 @@ async def createWorkspaceWorkflow(
|
|||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
"""Create a new empty workspace workflow."""
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
chatInterface = _getChatInterface(context, featureInstanceId=instanceId)
|
||||
_mandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
chatInterface = _getChatInterface(context, featureInstanceId=instanceId, mandateId=_mandateId)
|
||||
name = body.get("name", "Neuer Chat")
|
||||
workflow = chatInterface.createWorkflow({
|
||||
"featureInstanceId": instanceId,
|
||||
|
|
@ -1112,8 +1113,8 @@ async def getWorkspaceMessages(
|
|||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
"""Get all messages for a workspace workflow/conversation."""
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
chatInterface = _getChatInterface(context, featureInstanceId=instanceId)
|
||||
_mandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
chatInterface = _getChatInterface(context, featureInstanceId=instanceId, mandateId=_mandateId)
|
||||
messages = chatInterface.getMessages(workflowId) or []
|
||||
items = [_workspaceMessageToClientDict(m) for m in messages]
|
||||
items.sort(
|
||||
|
|
@ -1140,7 +1141,7 @@ async def listWorkspaceFiles(
|
|||
search: Optional[str] = Query(None),
|
||||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
_mandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
dbMgmt = _getDbManagement(context, featureInstanceId=instanceId)
|
||||
files = dbMgmt.getAllFiles()
|
||||
|
||||
|
|
@ -1172,7 +1173,7 @@ async def getFileContent(
|
|||
):
|
||||
"""Return the raw content of a file for preview."""
|
||||
from fastapi.responses import Response
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
_mandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
dbMgmt = _getDbManagement(context, featureInstanceId=instanceId)
|
||||
fileRecord = dbMgmt.getFile(fileId)
|
||||
if not fileRecord:
|
||||
|
|
@ -1198,13 +1199,13 @@ async def listWorkspaceFolders(
|
|||
parentId: Optional[str] = Query(None),
|
||||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
_mandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
try:
|
||||
from modules.serviceCenter import getService
|
||||
from modules.serviceCenter.context import ServiceCenterContext
|
||||
ctx = ServiceCenterContext(
|
||||
user=context.user,
|
||||
mandate_id=str(context.mandateId) if context.mandateId else None,
|
||||
mandate_id=_mandateId or "",
|
||||
feature_instance_id=instanceId,
|
||||
)
|
||||
chatService = getService("chat", ctx)
|
||||
|
|
@ -1243,12 +1244,12 @@ async def listWorkspaceConnections(
|
|||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
"""Return the user's active connections (UserConnections)."""
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
_mandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
from modules.serviceCenter import getService
|
||||
from modules.serviceCenter.context import ServiceCenterContext
|
||||
ctx = ServiceCenterContext(
|
||||
user=context.user,
|
||||
mandate_id=str(context.mandateId) if context.mandateId else None,
|
||||
mandate_id=_mandateId or "",
|
||||
feature_instance_id=instanceId,
|
||||
)
|
||||
chatService = getService("chat", ctx)
|
||||
|
|
@ -1290,12 +1291,12 @@ async def createWorkspaceDataSource(
|
|||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
"""Create a new DataSource for this workspace instance."""
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
_mandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
from modules.serviceCenter import getService
|
||||
from modules.serviceCenter.context import ServiceCenterContext
|
||||
ctx = ServiceCenterContext(
|
||||
user=context.user,
|
||||
mandate_id=str(context.mandateId) if context.mandateId else None,
|
||||
mandate_id=_mandateId or "",
|
||||
feature_instance_id=instanceId,
|
||||
)
|
||||
chatService = getService("chat", ctx)
|
||||
|
|
@ -1319,12 +1320,12 @@ async def deleteWorkspaceDataSource(
|
|||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
"""Delete a DataSource."""
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
_mandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
from modules.serviceCenter import getService
|
||||
from modules.serviceCenter.context import ServiceCenterContext
|
||||
ctx = ServiceCenterContext(
|
||||
user=context.user,
|
||||
mandate_id=str(context.mandateId) if context.mandateId else None,
|
||||
mandate_id=_mandateId or "",
|
||||
feature_instance_id=instanceId,
|
||||
)
|
||||
chatService = getService("chat", ctx)
|
||||
|
|
@ -1466,7 +1467,7 @@ async def listFeatureConnectionTables(
|
|||
node = {
|
||||
"objectKey": obj.get("objectKey", ""),
|
||||
"tableName": meta.get("table", ""),
|
||||
"label": obj.get("label", {}),
|
||||
"label": t(obj.get("label", "")),
|
||||
"fields": meta.get("fields", []),
|
||||
}
|
||||
if meta.get("isParent"):
|
||||
|
|
@ -1662,7 +1663,7 @@ async def deleteFeatureDataSource(
|
|||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
"""Delete a FeatureDataSource."""
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
_mandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
from modules.datamodels.datamodelFeatureDataSource import FeatureDataSource
|
||||
|
||||
|
|
@ -1680,14 +1681,14 @@ async def listConnectionServices(
|
|||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
"""Return the available services for a specific UserConnection."""
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
_mandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
try:
|
||||
from modules.connectors.connectorResolver import ConnectorResolver
|
||||
from modules.serviceCenter import getService as getSvc
|
||||
from modules.serviceCenter.context import ServiceCenterContext
|
||||
ctx = ServiceCenterContext(
|
||||
user=context.user,
|
||||
mandate_id=str(context.mandateId) if context.mandateId else None,
|
||||
mandate_id=_mandateId or "",
|
||||
feature_instance_id=instanceId,
|
||||
)
|
||||
chatService = getSvc("chat", ctx)
|
||||
|
|
@ -1739,14 +1740,14 @@ async def browseConnectionService(
|
|||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
"""Browse folders/items within a connection's service at a given path."""
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
_mandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
try:
|
||||
from modules.connectors.connectorResolver import ConnectorResolver
|
||||
from modules.serviceCenter import getService as getSvc
|
||||
from modules.serviceCenter.context import ServiceCenterContext
|
||||
ctx = ServiceCenterContext(
|
||||
user=context.user,
|
||||
mandate_id=str(context.mandateId) if context.mandateId else None,
|
||||
mandate_id=_mandateId or "",
|
||||
feature_instance_id=instanceId,
|
||||
)
|
||||
chatService = getSvc("chat", ctx)
|
||||
|
|
@ -1784,7 +1785,7 @@ async def transcribeVoice(
|
|||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
"""Transcribe audio to text using speech-to-text."""
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
_mandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
audioBytes = await audio.read()
|
||||
try:
|
||||
import aiohttp
|
||||
|
|
@ -1813,7 +1814,7 @@ async def synthesizeVoice(
|
|||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
"""Synthesize text to speech audio."""
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
_mandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
text = body.get("text", "")
|
||||
if not text:
|
||||
raise HTTPException(status_code=400, detail=routeApiMsg("text is required"))
|
||||
|
|
@ -1835,7 +1836,7 @@ async def getPendingEdits(
|
|||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
"""Return all pending file edit proposals for this workspace instance."""
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
_mandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
editList = [e.model_dump() for e in _pendingEditsStore.forInstance(instanceId).getPending()]
|
||||
return JSONResponse({"edits": editList})
|
||||
|
||||
|
|
@ -1849,7 +1850,7 @@ async def acceptEdit(
|
|||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
"""Accept a proposed file edit -- applies the new content to the file."""
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
_mandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
edit = _pendingEditsStore.forInstance(instanceId).get(editId)
|
||||
if not edit:
|
||||
raise HTTPException(status_code=404, detail=f"Edit proposal {editId} not found")
|
||||
|
|
@ -1886,7 +1887,7 @@ async def rejectEdit(
|
|||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
"""Reject a proposed file edit -- discards the change."""
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
_mandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
edit = _pendingEditsStore.forInstance(instanceId).get(editId)
|
||||
if not edit:
|
||||
raise HTTPException(status_code=404, detail=f"Edit proposal {editId} not found")
|
||||
|
|
@ -1911,7 +1912,7 @@ async def acceptAllEdits(
|
|||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
"""Accept all pending file edit proposals for this instance."""
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
_mandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
instanceEdits = _pendingEditsStore.forInstance(instanceId)
|
||||
dbMgmt = _getDbManagement(context, instanceId)
|
||||
accepted = []
|
||||
|
|
@ -1942,7 +1943,7 @@ async def rejectAllEdits(
|
|||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
"""Reject all pending file edit proposals for this instance."""
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
_mandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
instanceEdits = _pendingEditsStore.forInstance(instanceId)
|
||||
rejected = []
|
||||
|
||||
|
|
@ -1998,7 +1999,7 @@ async def updateGeneralSettings(
|
|||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
"""Update general workspace settings for the current user."""
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
_mandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
wsInterface = _getWorkspaceInterface(context, instanceId)
|
||||
userId = str(context.user.id)
|
||||
|
||||
|
|
|
|||
|
|
@ -3777,8 +3777,8 @@ class AppObjects:
|
|||
if conflictingRole and conflictingRole.id != roleId:
|
||||
raise ValueError(f"Role with label '{role.roleLabel}' already exists")
|
||||
|
||||
# Exclude id from model_dump - the URL roleId is authoritative
|
||||
updatedRole = self.db.recordModify(Role, roleId, role.model_dump(exclude={"id"}))
|
||||
_IMMUTABLE_ROLE_FIELDS = {"id", "mandateId", "featureInstanceId", "featureCode", "isSystemRole"}
|
||||
updatedRole = self.db.recordModify(Role, roleId, role.model_dump(exclude=_IMMUTABLE_ROLE_FIELDS))
|
||||
logger.info(f"Updated role with ID {roleId}")
|
||||
return Role(**updatedRole)
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -678,11 +678,15 @@ class ChatObjects:
|
|||
return list(matchedIds)
|
||||
|
||||
def getWorkflow(self, workflowId: str) -> Optional[ChatWorkflow]:
|
||||
"""Returns a workflow by ID if user has access."""
|
||||
# Use RBAC filtering with featureInstanceId for instance-level isolation
|
||||
"""Returns a workflow by ID if user has access.
|
||||
|
||||
Returns None when the workflow does not exist / RBAC denies access
|
||||
or when the stored data fails model validation (logged separately).
|
||||
"""
|
||||
workflows = self._getRecordset(ChatWorkflow, recordFilter={"id": workflowId})
|
||||
|
||||
if not workflows:
|
||||
logger.debug(f"getWorkflow: no record for {workflowId} (RBAC filter or not found)")
|
||||
return None
|
||||
|
||||
workflow = workflows[0]
|
||||
|
|
@ -690,8 +694,6 @@ class ChatObjects:
|
|||
logs = self.getLogs(workflowId)
|
||||
messages = self.getMessages(workflowId)
|
||||
|
||||
# Validate workflow data against ChatWorkflow model
|
||||
# Explicit type coercion: DB may store numeric fields as TEXT on some platforms
|
||||
def _toInt(v, default=0):
|
||||
try:
|
||||
return int(v) if v is not None else default
|
||||
|
|
@ -719,7 +721,7 @@ class ChatObjects:
|
|||
messages=messages
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating workflow data: {str(e)}")
|
||||
logger.error(f"getWorkflow: data validation failed for {workflowId}: {e}")
|
||||
return None
|
||||
|
||||
def createWorkflow(self, workflowData: Dict[str, Any]) -> ChatWorkflow:
|
||||
|
|
@ -1040,6 +1042,10 @@ class ChatObjects:
|
|||
workflowId = messageData["workflowId"]
|
||||
workflow = self.getWorkflow(workflowId)
|
||||
if not workflow:
|
||||
logger.warning(
|
||||
f"createMessage: workflow {workflowId} returned None "
|
||||
f"(RBAC denied, not found, or data validation failed — see preceding logs)"
|
||||
)
|
||||
raise PermissionError(f"No access to workflow {workflowId}")
|
||||
|
||||
if not self.checkRbacPermission(ChatWorkflow, "update", workflowId):
|
||||
|
|
|
|||
|
|
@ -252,7 +252,10 @@ class FeatureInterface:
|
|||
graph = json.loads(graphJson)
|
||||
|
||||
labelDict = template.get("label", {})
|
||||
label = labelDict.get("de") or labelDict.get("en") or str(labelDict) if isinstance(labelDict, dict) else str(labelDict)
|
||||
if isinstance(labelDict, dict):
|
||||
label = labelDict.get("xx") or next(iter(labelDict.values()), "") or str(labelDict)
|
||||
else:
|
||||
label = str(labelDict)
|
||||
|
||||
geInterface.createWorkflow({
|
||||
"label": label,
|
||||
|
|
|
|||
|
|
@ -3263,6 +3263,121 @@
|
|||
"key": "Überwachen Sie automatisch 100% der Gespräche, um wertvolle Einblicke für Ihr Unternehmen zu erhalten.",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "(gefiltert nach {name})",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "({count} gefiltert)",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Abonnement, Einstellungen und Guthaben pro Mandant",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Abrechnung",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Aktion",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Benutzer-Billing",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Benutzer-Guthaben",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Benutzer:",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Deaktiviert",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Du hast Zugriff auf {instanceCount} {instanceWord} in {mandateCount} {mandateWord}.",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Einstellungen gespeichert!",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Feature-Instanz",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Feature-Instanzen",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Fehler beim Speichern",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Gesamtguthaben",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Mandant:",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Mandanten",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Mandanten-Billing",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Mandanten-Guthaben",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Mandant",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Niedrig",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Transaktionen",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Warnschwelle",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "✓ Mandat eingereicht",
|
||||
|
|
@ -6536,6 +6651,121 @@
|
|||
"key": "Überwachen Sie automatisch 100% der Gespräche, um wertvolle Einblicke für Ihr Unternehmen zu erhalten.",
|
||||
"value": "Überwachen Sie automatisch 100% der Gespräche, um wertvolle Einblicke für Ihr Unternehmen zu erhalten."
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "(gefiltert nach {name})",
|
||||
"value": "(gefiltert nach {name})"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "({count} gefiltert)",
|
||||
"value": "({count} gefiltert)"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Abonnement, Einstellungen und Guthaben pro Mandant",
|
||||
"value": "Abonnement, Einstellungen und Guthaben pro Mandant"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Abrechnung",
|
||||
"value": "Abrechnung"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Aktion",
|
||||
"value": "Aktion"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Benutzer-Billing",
|
||||
"value": "Benutzer-Billing"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Benutzer-Guthaben",
|
||||
"value": "Benutzer-Guthaben"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Benutzer:",
|
||||
"value": "Benutzer:"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Deaktiviert",
|
||||
"value": "Deaktiviert"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Du hast Zugriff auf {instanceCount} {instanceWord} in {mandateCount} {mandateWord}.",
|
||||
"value": "Du hast Zugriff auf {instanceCount} {instanceWord} in {mandateCount} {mandateWord}."
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Einstellungen gespeichert!",
|
||||
"value": "Einstellungen gespeichert!"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Feature-Instanz",
|
||||
"value": "Feature-Instanz"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Feature-Instanzen",
|
||||
"value": "Feature-Instanzen"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Fehler beim Speichern",
|
||||
"value": "Fehler beim Speichern"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Gesamtguthaben",
|
||||
"value": "Gesamtguthaben"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Mandant:",
|
||||
"value": "Mandant:"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Mandanten",
|
||||
"value": "Mandanten"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Mandanten-Billing",
|
||||
"value": "Mandanten-Billing"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Mandanten-Guthaben",
|
||||
"value": "Mandanten-Guthaben"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Mandant",
|
||||
"value": "Mandant"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Niedrig",
|
||||
"value": "Niedrig"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Transaktionen",
|
||||
"value": "Transaktionen"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Warnschwelle",
|
||||
"value": "Warnschwelle"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "✓ Mandat eingereicht",
|
||||
|
|
@ -9634,6 +9864,121 @@
|
|||
"key": "Überwachen Sie automatisch 100% der Gespräche, um wertvolle Einblicke für Ihr Unternehmen zu erhalten.",
|
||||
"value": "Automatically monitor 100% of conversations to get valuable insights for your business."
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "(gefiltert nach {name})",
|
||||
"value": "(filtered by {name})"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "({count} gefiltert)",
|
||||
"value": "({count} filtered)"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Abonnement, Einstellungen und Guthaben pro Mandant",
|
||||
"value": "Subscription, settings, and credit per tenant"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Abrechnung",
|
||||
"value": "Billing"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Aktion",
|
||||
"value": "Action"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Benutzer-Billing",
|
||||
"value": "User billing"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Benutzer-Guthaben",
|
||||
"value": "User credits"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Benutzer:",
|
||||
"value": "User:"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Deaktiviert",
|
||||
"value": "Disabled"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Du hast Zugriff auf {instanceCount} {instanceWord} in {mandateCount} {mandateWord}.",
|
||||
"value": "You have access to {instanceCount} {instanceWord} in {mandateCount} {mandateWord}."
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Einstellungen gespeichert!",
|
||||
"value": "Settings saved!"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Feature-Instanz",
|
||||
"value": "Feature instance"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Feature-Instanzen",
|
||||
"value": "Feature instances"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Fehler beim Speichern",
|
||||
"value": "Error saving"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Gesamtguthaben",
|
||||
"value": "Total credit"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Mandant:",
|
||||
"value": "Tenant:"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Mandanten",
|
||||
"value": "Tenants"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Mandanten-Billing",
|
||||
"value": "Tenant billing"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Mandanten-Guthaben",
|
||||
"value": "Tenant credits"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Mandant",
|
||||
"value": "Tenant"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Niedrig",
|
||||
"value": "Low"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Transaktionen",
|
||||
"value": "Transactions"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Warnschwelle",
|
||||
"value": "Warning threshold"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "✓ Mandat eingereicht",
|
||||
|
|
@ -12732,6 +13077,121 @@
|
|||
"key": "Überwachen Sie automatisch 100% der Gespräche, um wertvolle Einblicke für Ihr Unternehmen zu erhalten.",
|
||||
"value": "Surveillez automatiquement 100% des conversations pour obtenir des insights précieux pour votre entreprise."
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "(gefiltert nach {name})",
|
||||
"value": "(filtré par {name})"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "({count} gefiltert)",
|
||||
"value": "({count} filtrés)"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Abonnement, Einstellungen und Guthaben pro Mandant",
|
||||
"value": "Abonnement, paramètres et crédits par client"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Abrechnung",
|
||||
"value": "Facturation"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Aktion",
|
||||
"value": "Action"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Benutzer-Billing",
|
||||
"value": "Facturation utilisateurs"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Benutzer-Guthaben",
|
||||
"value": "Crédits utilisateur"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Benutzer:",
|
||||
"value": "Utilisateur :"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Deaktiviert",
|
||||
"value": "Désactivé"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Du hast Zugriff auf {instanceCount} {instanceWord} in {mandateCount} {mandateWord}.",
|
||||
"value": "Vous avez accès à {instanceCount} {instanceWord} sur {mandateCount} {mandateWord}."
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Einstellungen gespeichert!",
|
||||
"value": "Paramètres enregistrés !"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Feature-Instanz",
|
||||
"value": "instance de fonctionnalité"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Feature-Instanzen",
|
||||
"value": "instances de fonctionnalité"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Fehler beim Speichern",
|
||||
"value": "Erreur lors de l'enregistrement"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Gesamtguthaben",
|
||||
"value": "Crédit total"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Mandant:",
|
||||
"value": "Client :"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Mandanten",
|
||||
"value": "Clients"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Mandanten-Billing",
|
||||
"value": "Facturation clients"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Mandanten-Guthaben",
|
||||
"value": "Crédits clients"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Mandant",
|
||||
"value": "Client"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Niedrig",
|
||||
"value": "Faible"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Transaktionen",
|
||||
"value": "Transactions"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "Warnschwelle",
|
||||
"value": "Seuil d'alerte"
|
||||
},
|
||||
{
|
||||
"context": "ui",
|
||||
"key": "✓ Mandat eingereicht",
|
||||
|
|
|
|||
|
|
@ -33,12 +33,15 @@ routeApiMsg = apiRouteContext("routeAdminFeatures")
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _featureLabelPlain(label: Union[str, Dict[str, str], None], fallback: str) -> str:
|
||||
"""Catalog feature label as German i18n key string."""
|
||||
def _featureLabelPlain(label: Union[str, Dict[str, str], None], fallback: str, requestLang: Optional[str] = None) -> str:
|
||||
"""Catalog feature label as a single display/i18n key string."""
|
||||
if isinstance(label, str) and label.strip():
|
||||
return label
|
||||
if isinstance(label, dict):
|
||||
return label.get("de") or label.get("en") or fallback
|
||||
picked = (requestLang and label.get(requestLang)) or label.get("xx") or next(iter(label.values()), "")
|
||||
if picked:
|
||||
return str(picked)
|
||||
return fallback
|
||||
return fallback
|
||||
|
||||
|
||||
|
|
@ -194,7 +197,11 @@ def get_my_feature_instances(
|
|||
featureDef = catalogService.getFeatureDefinition(instance.featureCode)
|
||||
featuresMap[featureKey] = {
|
||||
"code": instance.featureCode,
|
||||
"label": _featureLabelPlain(featureDef.get("label") if featureDef else None, instance.featureCode),
|
||||
"label": _featureLabelPlain(
|
||||
featureDef.get("label") if featureDef else None,
|
||||
instance.featureCode,
|
||||
getattr(context.user, "language", None),
|
||||
),
|
||||
"icon": featureDef.get("icon", "folder") if featureDef else "folder",
|
||||
"instances": [],
|
||||
"_mandateId": mandateId # Temporary for grouping
|
||||
|
|
|
|||
|
|
@ -23,12 +23,23 @@ from modules.datamodels.datamodelRbac import AccessRuleContext, AccessRule, Role
|
|||
from modules.datamodels.datamodelMembership import UserMandate
|
||||
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
|
||||
from modules.interfaces.interfaceDbApp import getInterface, getRootInterface
|
||||
from modules.shared.i18nRegistry import apiRouteContext
|
||||
from modules.shared.i18nRegistry import apiRouteContext, t, _getLanguage
|
||||
routeApiMsg = apiRouteContext("routeAdminRbacRules")
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _resolveTextMultilingual(value) -> str:
|
||||
"""Resolve a TextMultilingual dict to a single string for the current request language.
|
||||
Falls back to xx (source text), then any available value."""
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
lang = _getLanguage()
|
||||
return value.get(lang) or value.get("xx") or next(iter(value.values()), "")
|
||||
return str(value) if value else ""
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/rbac",
|
||||
tags=["RBAC"],
|
||||
|
|
@ -911,7 +922,7 @@ def list_roles(
|
|||
result.append({
|
||||
"id": role.id,
|
||||
"roleLabel": role.roleLabel,
|
||||
"description": role.description,
|
||||
"description": _resolveTextMultilingual(role.description),
|
||||
"mandateId": role.mandateId,
|
||||
"featureInstanceId": role.featureInstanceId,
|
||||
"featureCode": role.featureCode,
|
||||
|
|
@ -931,14 +942,8 @@ def list_roles(
|
|||
if searchTerm:
|
||||
searchedResult = []
|
||||
for item in result:
|
||||
# Search in roleLabel and description
|
||||
roleLabel = (item.get("roleLabel") or "").lower()
|
||||
description = item.get("description")
|
||||
descText = ""
|
||||
if isinstance(description, dict):
|
||||
descText = " ".join(str(v) for v in description.values()).lower()
|
||||
elif description:
|
||||
descText = str(description).lower()
|
||||
descText = (item.get("description") or "").lower()
|
||||
scopeType = (item.get("scopeType") or "").lower()
|
||||
|
||||
if searchTerm in roleLabel or searchTerm in descText or searchTerm in scopeType:
|
||||
|
|
@ -1046,7 +1051,7 @@ def get_roles_filter_values(
|
|||
result.append({
|
||||
"id": role.id,
|
||||
"roleLabel": role.roleLabel,
|
||||
"description": role.description,
|
||||
"description": _resolveTextMultilingual(role.description),
|
||||
"mandateId": role.mandateId,
|
||||
"featureInstanceId": role.featureInstanceId,
|
||||
"featureCode": role.featureCode,
|
||||
|
|
@ -1163,7 +1168,7 @@ def get_role(
|
|||
return {
|
||||
"id": role.id,
|
||||
"roleLabel": role.roleLabel,
|
||||
"description": role.description,
|
||||
"description": _resolveTextMultilingual(role.description),
|
||||
"mandateId": role.mandateId,
|
||||
"featureInstanceId": role.featureInstanceId,
|
||||
"featureCode": role.featureCode,
|
||||
|
|
@ -1362,6 +1367,16 @@ def getCatalogObjects(
|
|||
except Exception as e:
|
||||
logger.warning(f"Could not get active features for mandate {mandateId}: {e}")
|
||||
|
||||
def _resolveLabels(objects: list) -> list:
|
||||
for obj in objects:
|
||||
raw = obj.get("label")
|
||||
if isinstance(raw, str):
|
||||
obj["label"] = t(raw)
|
||||
elif isinstance(raw, dict):
|
||||
key = raw.get("xx") or next(iter(raw.values()), "") or obj.get("objectKey", "")
|
||||
obj["label"] = t(key) if key else f"[{obj.get('objectKey', '?')}]"
|
||||
return objects
|
||||
|
||||
if context:
|
||||
# Single context filter
|
||||
try:
|
||||
|
|
@ -1383,7 +1398,7 @@ def getCatalogObjects(
|
|||
if activeFeatures:
|
||||
objects = [obj for obj in objects if obj.get("featureCode") in activeFeatures]
|
||||
|
||||
return {context.upper(): objects}
|
||||
return {context.upper(): _resolveLabels(objects)}
|
||||
else:
|
||||
# All contexts
|
||||
result = catalog.getAllCatalogObjects(featureCode)
|
||||
|
|
@ -1393,6 +1408,8 @@ def getCatalogObjects(
|
|||
for ctxKey in result:
|
||||
result[ctxKey] = [obj for obj in result[ctxKey] if obj.get("featureCode") in activeFeatures]
|
||||
|
||||
for ctxKey in result:
|
||||
_resolveLabels(result[ctxKey])
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
|
|
|
|||
|
|
@ -24,13 +24,24 @@ from modules.datamodels.datamodelMembership import (
|
|||
)
|
||||
from modules.datamodels.datamodelFeatures import FeatureInstance, Feature
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
from modules.shared.i18nRegistry import apiRouteContext
|
||||
from modules.shared.i18nRegistry import apiRouteContext, t, _getLanguage
|
||||
routeApiMsg = apiRouteContext("routeAdminUserAccessOverview")
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _resolveTextMultilingual(value) -> str:
|
||||
"""Resolve a TextMultilingual dict to a single string for the current request language.
|
||||
Falls back to xx (source text), then any available value."""
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
lang = _getLanguage()
|
||||
return value.get(lang) or value.get("xx") or next(iter(value.values()), "")
|
||||
return str(value) if value else ""
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/admin/user-access-overview",
|
||||
tags=["Admin User Access Overview"],
|
||||
|
|
@ -298,7 +309,7 @@ def getUserAccessOverview(
|
|||
roleInfo = {
|
||||
"id": roleId,
|
||||
"roleLabel": role.roleLabel,
|
||||
"description": role.description or {},
|
||||
"description": _resolveTextMultilingual(role.description),
|
||||
"scope": scope,
|
||||
"scopePriority": _getRoleScopePriority(scope),
|
||||
"mandateId": role.mandateId,
|
||||
|
|
@ -334,7 +345,7 @@ def getUserAccessOverview(
|
|||
# Get feature info using interface method
|
||||
featureCode = instance.featureCode
|
||||
feature = interface.getFeatureByCode(featureCode)
|
||||
featureLabel = feature.label if feature else {}
|
||||
featureLabel = t(feature.label) if feature and feature.label else ""
|
||||
|
||||
# Get roles for this FeatureAccess using interface method
|
||||
instanceRoleIds = interface.getRoleIdsForFeatureAccess(faId)
|
||||
|
|
@ -348,7 +359,7 @@ def getUserAccessOverview(
|
|||
roleInfo = {
|
||||
"id": roleId,
|
||||
"roleLabel": role.roleLabel,
|
||||
"description": role.description or {},
|
||||
"description": _resolveTextMultilingual(role.description),
|
||||
"scope": scope,
|
||||
"scopePriority": _getRoleScopePriority(scope),
|
||||
"mandateId": role.mandateId,
|
||||
|
|
|
|||
|
|
@ -143,6 +143,8 @@ async def _autoIndexFile(fileId: str, fileName: str, mimeType: str, user):
|
|||
structure=contentIndex.structure,
|
||||
)
|
||||
|
||||
# Re-acquire interface after await to avoid stale user context from the singleton
|
||||
mgmtInterface = interfaceDbManagement.getInterface(user)
|
||||
mgmtInterface.updateFile(fileId, {"status": "active"})
|
||||
logger.info(f"Auto-index complete for file {fileId} ({fileName})")
|
||||
|
||||
|
|
|
|||
|
|
@ -779,7 +779,7 @@ def add_user_to_mandate(
|
|||
f"with roles {data.roleIds}"
|
||||
)
|
||||
|
||||
mname = _mandate_display_name(mandate)
|
||||
mname = _mandate_display_name(mandate, getattr(targetUser, "language", None))
|
||||
create_access_change_notification(
|
||||
data.targetUserId,
|
||||
"Mandantenzugriff",
|
||||
|
|
@ -877,7 +877,9 @@ def remove_user_from_mandate(
|
|||
|
||||
logger.info(f"User {context.user.id} removed user {targetUserId} from mandate {targetMandateId}")
|
||||
|
||||
mname = _mandate_display_name(mandate)
|
||||
removedUser = rootInterface.getUser(targetUserId)
|
||||
notifyLang = getattr(removedUser, "language", None) if removedUser else getattr(context.user, "language", None)
|
||||
mname = _mandate_display_name(mandate, notifyLang)
|
||||
create_access_change_notification(
|
||||
targetUserId,
|
||||
"Mandantenzugriff",
|
||||
|
|
@ -982,7 +984,9 @@ def update_user_roles_in_mandate(
|
|||
)
|
||||
|
||||
mandate_meta = rootInterface.getMandate(targetMandateId)
|
||||
mname = _mandate_display_name(mandate_meta)
|
||||
roleUser = rootInterface.getUser(targetUserId)
|
||||
notifyLang = getattr(roleUser, "language", None) if roleUser else getattr(context.user, "language", None)
|
||||
mname = _mandate_display_name(mandate_meta, notifyLang)
|
||||
create_access_change_notification(
|
||||
targetUserId,
|
||||
"Mandantenrollen geändert",
|
||||
|
|
@ -1013,7 +1017,7 @@ def update_user_roles_in_mandate(
|
|||
# Helper Functions
|
||||
# =============================================================================
|
||||
|
||||
def _mandate_display_name(mandate: Any) -> str:
|
||||
def _mandate_display_name(mandate: Any, requestLang: Optional[str] = None) -> str:
|
||||
"""Human-readable mandate label for notifications."""
|
||||
if mandate is None:
|
||||
return ""
|
||||
|
|
@ -1022,14 +1026,16 @@ def _mandate_display_name(mandate: Any) -> str:
|
|||
return str(mandate["label"])
|
||||
name = mandate.get("name")
|
||||
if isinstance(name, dict):
|
||||
return str(name.get("de") or name.get("en") or (next(iter(name.values()), "") if name else ""))
|
||||
picked = (requestLang and name.get(requestLang)) or name.get("xx") or next(iter(name.values()), "")
|
||||
return str(picked) if picked is not None else ""
|
||||
return str(name or mandate.get("id", ""))
|
||||
label = getattr(mandate, "label", None)
|
||||
if label:
|
||||
return str(label)
|
||||
name = getattr(mandate, "name", None)
|
||||
if isinstance(name, dict):
|
||||
return str(name.get("de") or name.get("en") or (next(iter(name.values()), "") if name else ""))
|
||||
picked = (requestLang and name.get(requestLang)) or name.get("xx") or next(iter(name.values()), "")
|
||||
return str(picked) if picked is not None else ""
|
||||
if name is not None:
|
||||
return str(name)
|
||||
return str(getattr(mandate, "id", ""))
|
||||
|
|
|
|||
|
|
@ -71,7 +71,11 @@ def _isAdminForUser(context: RequestContext, targetUserId: str) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
def _extractDistinctValues(items: List[Dict[str, Any]], columnKey: str) -> List[str]:
|
||||
def _extractDistinctValues(
|
||||
items: List[Dict[str, Any]],
|
||||
columnKey: str,
|
||||
requestLang: Optional[str] = None,
|
||||
) -> List[str]:
|
||||
"""Extract sorted distinct display values for a column from enriched items."""
|
||||
values = set()
|
||||
for item in items:
|
||||
|
|
@ -83,7 +87,7 @@ def _extractDistinctValues(items: List[Dict[str, Any]], columnKey: str) -> List[
|
|||
elif isinstance(val, (int, float)):
|
||||
values.add(str(val))
|
||||
elif isinstance(val, dict):
|
||||
text = val.get("en") or next((v for v in val.values() if isinstance(v, str) and v), None)
|
||||
text = (requestLang and val.get(requestLang)) or val.get("xx") or next(iter(val.values()), None)
|
||||
if text:
|
||||
values.add(str(text))
|
||||
else:
|
||||
|
|
@ -95,6 +99,7 @@ def _handleFilterValuesRequest(
|
|||
items: List[Dict[str, Any]],
|
||||
column: str,
|
||||
paginationJson: Optional[str] = None,
|
||||
requestLang: Optional[str] = None,
|
||||
) -> List[str]:
|
||||
"""
|
||||
Generic handler for /filter-values endpoints.
|
||||
|
|
@ -117,7 +122,7 @@ def _handleFilterValuesRequest(
|
|||
pass
|
||||
|
||||
crossFiltered = _applyFiltersAndSort(items, crossFilterParams)
|
||||
return _extractDistinctValues(crossFiltered, column)
|
||||
return _extractDistinctValues(crossFiltered, column, requestLang)
|
||||
|
||||
|
||||
def _applyFiltersAndSort(items: List[Dict[str, Any]], paginationParams: Optional[PaginationParams]) -> List[Dict[str, Any]]:
|
||||
|
|
@ -507,7 +512,7 @@ def get_user_filter_values(
|
|||
result = appInterface.getUsersByMandate(str(context.mandateId), None)
|
||||
users = result if isinstance(result, list) else (result.items if hasattr(result, 'items') else [])
|
||||
items = [u.model_dump() if hasattr(u, 'model_dump') else u for u in users]
|
||||
return _handleFilterValuesRequest(items, column, pagination)
|
||||
return _handleFilterValuesRequest(items, column, pagination, getattr(context.user, "language", None))
|
||||
elif context.hasSysAdminRole:
|
||||
# SysAdmin: use SQL DISTINCT for DB columns
|
||||
try:
|
||||
|
|
@ -519,7 +524,7 @@ def get_user_filter_values(
|
|||
except Exception:
|
||||
users = appInterface.getAllUsers()
|
||||
items = [u.model_dump() if hasattr(u, 'model_dump') else u for u in users]
|
||||
return _handleFilterValuesRequest(items, column, pagination)
|
||||
return _handleFilterValuesRequest(items, column, pagination, getattr(context.user, "language", None))
|
||||
else:
|
||||
# Non-admin multi-mandate: aggregate across admin mandates (in-memory)
|
||||
rootInterface = getRootInterface()
|
||||
|
|
@ -547,7 +552,7 @@ def get_user_filter_values(
|
|||
})
|
||||
batchUsers = rootInterface.getUsersByIds(uniqueUserIds) if uniqueUserIds else {}
|
||||
items = [u.model_dump() if hasattr(u, 'model_dump') else vars(u) for u in batchUsers.values()]
|
||||
return _handleFilterValuesRequest(items, column, pagination)
|
||||
return _handleFilterValuesRequest(items, column, pagination, getattr(context.user, "language", None))
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ from modules.datamodels.datamodelAi import (
|
|||
)
|
||||
from modules.datamodels.datamodelUiLanguage import I18nEntry, UiLanguageSet
|
||||
from modules.datamodels.datamodelUam import User
|
||||
from modules.datamodels.datamodelRbac import Role
|
||||
from modules.datamodels.datamodelFeatures import Feature
|
||||
from modules.datamodels.datamodelNotification import NotificationType
|
||||
from modules.interfaces.interfaceDbManagement import getInterface as getMgmtInterface
|
||||
from modules.routes.routeNotifications import _createNotification
|
||||
|
|
@ -532,6 +534,67 @@ def _validate_iso2_code(code: str) -> str:
|
|||
return c
|
||||
|
||||
|
||||
async def _translateTextMultilingualFields(db, langCode: str, langLabel: str, billingCb=None) -> int:
|
||||
"""Batch-translate all TextMultilingual fields (Role.description, Feature.label) for a new language."""
|
||||
textsToTranslate: Dict[str, str] = {}
|
||||
|
||||
roles = db.getRecordset(Role)
|
||||
for r in roles:
|
||||
desc = r.get("description") if isinstance(r, dict) else getattr(r, "description", None)
|
||||
if isinstance(desc, dict):
|
||||
sourceText = desc.get("xx", "")
|
||||
if sourceText and not desc.get(langCode):
|
||||
textsToTranslate[f"role:{r.get('id') if isinstance(r, dict) else r.id}:description"] = sourceText
|
||||
|
||||
features = db.getRecordset(Feature)
|
||||
for f in features:
|
||||
lbl = f.get("label") if isinstance(f, dict) else getattr(f, "label", None)
|
||||
if isinstance(lbl, dict):
|
||||
sourceText = lbl.get("xx", "")
|
||||
if sourceText and not lbl.get(langCode):
|
||||
textsToTranslate[f"feature:{f.get('code') if isinstance(f, dict) else f.code}:label"] = sourceText
|
||||
|
||||
if not textsToTranslate:
|
||||
return 0
|
||||
|
||||
keysForAi = {v: "User-generated content field" for v in textsToTranslate.values()}
|
||||
uniqueTexts = list(set(keysForAi.keys()))
|
||||
keysForAi = {t: "User-generated content field" for t in uniqueTexts}
|
||||
translated = await _translateBatch(keysForAi, langLabel, langCode, billingCallback=billingCb)
|
||||
|
||||
count = 0
|
||||
for compositeKey, deText in textsToTranslate.items():
|
||||
translatedText = translated.get(deText)
|
||||
if not translatedText:
|
||||
continue
|
||||
parts = compositeKey.split(":")
|
||||
if parts[0] == "role":
|
||||
roleId = parts[1]
|
||||
rows = db.getRecordset(Role, recordFilter={"id": roleId})
|
||||
if rows:
|
||||
rec = dict(rows[0]) if not isinstance(rows[0], dict) else rows[0]
|
||||
desc = rec.get("description", {})
|
||||
if isinstance(desc, dict):
|
||||
desc[langCode] = translatedText
|
||||
rec["description"] = desc
|
||||
db.recordModify(Role, roleId, rec)
|
||||
count += 1
|
||||
elif parts[0] == "feature":
|
||||
featureCode = parts[1]
|
||||
rows = db.getRecordset(Feature, recordFilter={"code": featureCode})
|
||||
if rows:
|
||||
rec = dict(rows[0]) if not isinstance(rows[0], dict) else rows[0]
|
||||
lbl = rec.get("label", {})
|
||||
if isinstance(lbl, dict):
|
||||
lbl[langCode] = translatedText
|
||||
rec["label"] = lbl
|
||||
db.recordModify(Feature, featureCode, rec)
|
||||
count += 1
|
||||
|
||||
logger.info("TextMultilingual batch translate: %d fields translated to %s", count, langCode)
|
||||
return count
|
||||
|
||||
|
||||
def _run_create_language_job(userId: str, code: str, label: str, currentUser: User, mandateId: str) -> None:
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
|
|
@ -580,14 +643,17 @@ async def _run_create_language_job_async(userId: str, code: str, label: str, cur
|
|||
db.recordModify(UiLanguageSet, code, merged)
|
||||
|
||||
statusHint = "" if finalStatus == "complete" else f" ({missingCount} Keys ohne Übersetzung)"
|
||||
|
||||
tmCount = await _translateTextMultilingualFields(db, code, label, billingCb)
|
||||
|
||||
_createNotification(
|
||||
userId,
|
||||
NotificationType.SYSTEM,
|
||||
title="Sprachset erstellt",
|
||||
message=f"Die Sprache «{label}» ({code}) wurde per KI übersetzt{statusHint}.",
|
||||
message=f"Die Sprache «{label}» ({code}) wurde per KI übersetzt{statusHint}. {tmCount} Inhaltsfelder übersetzt.",
|
||||
)
|
||||
await _reloadI18nCache()
|
||||
logger.info("i18n create job done: code=%s, translated=%d/%d", code, len(translated), len(xxEntries))
|
||||
logger.info("i18n create job done: code=%s, translated=%d/%d, tm_fields=%d", code, len(translated), len(xxEntries), tmCount)
|
||||
except Exception as e:
|
||||
logger.exception("create language job failed: %s", e)
|
||||
_createNotification(
|
||||
|
|
|
|||
|
|
@ -29,12 +29,15 @@ routeApiMsg = apiRouteContext("routeStore")
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _storeLabelText(label: Union[str, Dict[str, str], None], fallback: str) -> str:
|
||||
"""Normalize catalog label to German i18n key string."""
|
||||
def _storeLabelText(label: Union[str, Dict[str, str], None], fallback: str, requestLang: Optional[str] = None) -> str:
|
||||
"""Normalize catalog label to a single display/i18n key string."""
|
||||
if isinstance(label, str) and label.strip():
|
||||
return label
|
||||
if isinstance(label, dict):
|
||||
return label.get("de") or label.get("en") or fallback
|
||||
picked = (requestLang and label.get(requestLang)) or label.get("xx") or next(iter(label.values()), "")
|
||||
if picked:
|
||||
return str(picked)
|
||||
return fallback
|
||||
return fallback
|
||||
|
||||
router = APIRouter(
|
||||
|
|
@ -298,9 +301,9 @@ def listStoreFeatures(
|
|||
instances = _getUserInstancesForFeature(db, userId, featureCode, userMandateIds)
|
||||
result.append(StoreFeatureResponse(
|
||||
featureCode=featureCode,
|
||||
label=_storeLabelText(featureDef.get("label"), featureCode),
|
||||
label=_storeLabelText(featureDef.get("label"), featureCode, getattr(context.user, "language", None)),
|
||||
icon=featureDef.get("icon", "mdi-puzzle"),
|
||||
description=_storeLabelText(featureDef.get("description"), ""),
|
||||
description=_storeLabelText(featureDef.get("description"), "", getattr(context.user, "language", None)),
|
||||
instances=instances,
|
||||
canActivate=True,
|
||||
))
|
||||
|
|
@ -388,7 +391,7 @@ def activateStoreFeature(
|
|||
|
||||
# ── 3. Provision instance ───────────────────────────────────────
|
||||
featureInterface = getFeatureInterface(db)
|
||||
featureLabel = _storeLabelText(featureDef.get("label"), featureCode)
|
||||
featureLabel = _storeLabelText(featureDef.get("label"), featureCode, getattr(context.user, "language", None))
|
||||
instance = featureInterface.createFeatureInstance(
|
||||
featureCode=featureCode,
|
||||
mandateId=mandateId,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ Feature-Container register their RBAC objects via mainXxx.py at startup.
|
|||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional, Union
|
||||
from typing import Dict, List, Any, Optional
|
||||
from threading import Lock
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -43,7 +43,7 @@ class RbacCatalogService:
|
|||
self._initialized = True
|
||||
logger.info("RBAC Catalog Service initialized")
|
||||
|
||||
def registerUiObject(self, featureCode: str, objectKey: str, label: Union[str, Dict[str, str]], meta: Optional[Dict[str, Any]] = None) -> bool:
|
||||
def registerUiObject(self, featureCode: str, objectKey: str, label: str, meta: Optional[Dict[str, Any]] = None) -> bool:
|
||||
"""Register a UI object for a feature."""
|
||||
try:
|
||||
self._uiObjects[objectKey] = {"objectKey": objectKey, "featureCode": featureCode, "label": label, "meta": meta or {}, "type": "UI"}
|
||||
|
|
@ -52,7 +52,7 @@ class RbacCatalogService:
|
|||
logger.error(f"Failed to register UI object {objectKey}: {e}")
|
||||
return False
|
||||
|
||||
def registerResourceObject(self, featureCode: str, objectKey: str, label: Dict[str, str], meta: Optional[Dict[str, Any]] = None) -> bool:
|
||||
def registerResourceObject(self, featureCode: str, objectKey: str, label: str, meta: Optional[Dict[str, Any]] = None) -> bool:
|
||||
"""Register a RESOURCE object for a feature."""
|
||||
try:
|
||||
self._resourceObjects[objectKey] = {"objectKey": objectKey, "featureCode": featureCode, "label": label, "meta": meta or {}, "type": "RESOURCE"}
|
||||
|
|
@ -61,14 +61,14 @@ class RbacCatalogService:
|
|||
logger.error(f"Failed to register RESOURCE object {objectKey}: {e}")
|
||||
return False
|
||||
|
||||
def registerDataObject(self, featureCode: str, objectKey: str, label: Dict[str, str], meta: Optional[Dict[str, Any]] = None) -> bool:
|
||||
def registerDataObject(self, featureCode: str, objectKey: str, label: str, meta: Optional[Dict[str, Any]] = None) -> bool:
|
||||
"""
|
||||
Register a DATA object (table/entity) for a feature.
|
||||
|
||||
Args:
|
||||
featureCode: Feature code (e.g., "trustee", "system")
|
||||
objectKey: Dot-notation key (e.g., "data.feature.trustee.TrusteeContract")
|
||||
label: Multilingual label dict
|
||||
label: German plaintext label (used as i18n key)
|
||||
meta: Optional metadata (e.g., table name, fields list)
|
||||
"""
|
||||
try:
|
||||
|
|
@ -84,7 +84,7 @@ class RbacCatalogService:
|
|||
logger.error(f"Failed to register DATA object {objectKey}: {e}")
|
||||
return False
|
||||
|
||||
def registerFeatureDefinition(self, featureCode: str, label: Union[str, Dict[str, str]], icon: str) -> bool:
|
||||
def registerFeatureDefinition(self, featureCode: str, label: str, icon: str) -> bool:
|
||||
"""Register a feature definition."""
|
||||
try:
|
||||
self._featureDefinitions[featureCode] = {"code": featureCode, "label": label, "icon": icon}
|
||||
|
|
|
|||
|
|
@ -80,10 +80,13 @@ def _convertParameterSchema(actionParams: Dict[str, Any]) -> Dict[str, Any]:
|
|||
paramRequired = paramInfo.get("required", False) if isinstance(paramInfo, dict) else False
|
||||
|
||||
jsonType = _pythonTypeToJsonType(paramType)
|
||||
properties[paramName] = {
|
||||
prop: Dict[str, Any] = {
|
||||
"type": jsonType,
|
||||
"description": paramDesc
|
||||
"description": paramDesc,
|
||||
}
|
||||
if jsonType == "array":
|
||||
prop["items"] = _pythonTypeToArrayItems(paramType) or {"type": "string"}
|
||||
properties[paramName] = prop
|
||||
|
||||
if paramRequired:
|
||||
required.append(paramName)
|
||||
|
|
@ -95,9 +98,7 @@ def _convertParameterSchema(actionParams: Dict[str, Any]) -> Dict[str, Any]:
|
|||
}
|
||||
|
||||
|
||||
def _pythonTypeToJsonType(pythonType: str) -> str:
|
||||
"""Map Python type strings to JSON Schema types."""
|
||||
mapping = {
|
||||
_TYPE_MAPPING = {
|
||||
"str": "string",
|
||||
"int": "integer",
|
||||
"float": "number",
|
||||
|
|
@ -107,9 +108,27 @@ def _pythonTypeToJsonType(pythonType: str) -> str:
|
|||
"List[str]": "array",
|
||||
"List[int]": "array",
|
||||
"List[dict]": "array",
|
||||
"List[float]": "array",
|
||||
"Dict[str, Any]": "object",
|
||||
}
|
||||
return mapping.get(pythonType, "string")
|
||||
|
||||
_ARRAY_ITEMS_MAPPING = {
|
||||
"list": {"type": "string"},
|
||||
"List[str]": {"type": "string"},
|
||||
"List[int]": {"type": "integer"},
|
||||
"List[float]": {"type": "number"},
|
||||
"List[dict]": {"type": "object"},
|
||||
}
|
||||
|
||||
|
||||
def _pythonTypeToJsonType(pythonType: str) -> str:
|
||||
"""Map Python type strings to JSON Schema types."""
|
||||
return _TYPE_MAPPING.get(pythonType, "string")
|
||||
|
||||
|
||||
def _pythonTypeToArrayItems(pythonType: str) -> Optional[Dict[str, Any]]:
|
||||
"""Return the JSON Schema `items` descriptor for array types, or None."""
|
||||
return _ARRAY_ITEMS_MAPPING.get(pythonType)
|
||||
|
||||
|
||||
def _createDispatchHandler(actionExecutor, methodName: str, actionName: str):
|
||||
|
|
|
|||
|
|
@ -44,10 +44,12 @@ def _registerConnectionTools(registry: ToolRegistry, services):
|
|||
return ToolResult(toolCallId="", toolName="listConnections", success=True, data="No connections available.")
|
||||
lines = []
|
||||
for conn in connections:
|
||||
connId = conn.get("id", "?") if isinstance(conn, dict) else getattr(conn, "id", "?")
|
||||
authority = conn.get("authority", "?") if isinstance(conn, dict) else getattr(conn, "authority", "?")
|
||||
authorityVal = authority.value if hasattr(authority, "value") else str(authority)
|
||||
username = conn.get("externalUsername", "") if isinstance(conn, dict) else getattr(conn, "externalUsername", "")
|
||||
email = conn.get("externalEmail", "") if isinstance(conn, dict) else getattr(conn, "externalEmail", "")
|
||||
lines.append(f"- {authority} ({email}) id: {connId}")
|
||||
ref = f"connection:{authorityVal}:{username}"
|
||||
lines.append(f"- {ref} ({email})")
|
||||
return ToolResult(toolCallId="", toolName="listConnections", success=True, data="\n".join(lines))
|
||||
except Exception as e:
|
||||
return ToolResult(toolCallId="", toolName="listConnections", success=False, error=str(e))
|
||||
|
|
|
|||
|
|
@ -93,6 +93,11 @@ def _registerFeatureSubAgentTools(registry: ToolRegistry, services):
|
|||
instanceLabel = instance.label or ""
|
||||
userId = context.get("userId", "")
|
||||
workspaceInstanceId = context.get("featureInstanceId", "")
|
||||
requestLang = None
|
||||
if userId:
|
||||
langUser = rootIf.getUser(userId)
|
||||
if langUser:
|
||||
requestLang = getattr(langUser, "language", None)
|
||||
|
||||
rootDbConn = rootIf.db if hasattr(rootIf, "db") else None
|
||||
if rootDbConn is None:
|
||||
|
|
@ -165,6 +170,7 @@ def _registerFeatureSubAgentTools(registry: ToolRegistry, services):
|
|||
dbConnector=featureDbConn,
|
||||
instanceLabel=instanceLabel,
|
||||
tableFilters=tableFilters,
|
||||
requestLang=requestLang,
|
||||
)
|
||||
|
||||
_featureQueryCache[cacheKey] = (time.time(), answer)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ async def runFeatureDataAgent(
|
|||
dbConnector,
|
||||
instanceLabel: str = "",
|
||||
tableFilters: Optional[Dict[str, Dict[str, str]]] = None,
|
||||
requestLang: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Run the feature data sub-agent and return the textual result.
|
||||
|
||||
|
|
@ -53,6 +54,7 @@ async def runFeatureDataAgent(
|
|||
dbConnector: DatabaseConnector for queries.
|
||||
instanceLabel: Human-readable instance name for context.
|
||||
tableFilters: Per-table record filters from FeatureDataSource.recordFilter.
|
||||
requestLang: ISO 639-1 code for resolving multilingual table labels in the schema prompt.
|
||||
|
||||
Returns:
|
||||
Plain-text answer produced by the sub-agent.
|
||||
|
|
@ -69,7 +71,7 @@ async def runFeatureDataAgent(
|
|||
if realCols:
|
||||
meta["fields"] = realCols
|
||||
|
||||
systemPrompt = _buildSchemaContext(featureCode, instanceLabel, selectedTables)
|
||||
systemPrompt = _buildSchemaContext(featureCode, instanceLabel, selectedTables, requestLang)
|
||||
|
||||
config = AgentConfig(
|
||||
maxRounds=_MAX_ROUNDS,
|
||||
|
|
@ -293,6 +295,7 @@ def _buildSchemaContext(
|
|||
featureCode: str,
|
||||
instanceLabel: str,
|
||||
selectedTables: List[Dict[str, Any]],
|
||||
requestLang: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Build a system prompt describing available tables and query strategy."""
|
||||
tableNames = []
|
||||
|
|
@ -303,7 +306,11 @@ def _buildSchemaContext(
|
|||
tbl = meta.get("table", "?")
|
||||
fields = meta.get("fields", [])
|
||||
label = obj.get("label", {})
|
||||
labelStr = label.get("en") or label.get("de") or tbl
|
||||
if isinstance(label, dict):
|
||||
picked = (requestLang and label.get(requestLang)) or label.get("xx") or next(iter(label.values()), "")
|
||||
labelStr = str(picked) if picked else tbl
|
||||
else:
|
||||
labelStr = str(label).strip() if isinstance(label, str) and str(label).strip() else tbl
|
||||
tableNames.append(tbl)
|
||||
block = f" Table: {tbl} ({labelStr})"
|
||||
if fields:
|
||||
|
|
|
|||
|
|
@ -371,30 +371,36 @@ class ChatService:
|
|||
return None
|
||||
|
||||
def getUserConnectionFromConnectionReference(self, connectionReference: str) -> Optional[UserConnection]:
|
||||
"""Get UserConnection from reference string (handles new format without UUID)"""
|
||||
"""Get UserConnection from reference string.
|
||||
|
||||
Supported formats:
|
||||
- connection:{authority}:{username} [status:..., token:...]
|
||||
- A raw UUID (fallback: lookup by connection ID)
|
||||
"""
|
||||
try:
|
||||
# Parse reference format: connection:{authority}:{username} [status:..., token:...]
|
||||
# Remove state information if present
|
||||
base_reference = connectionReference.split(' [')[0]
|
||||
base_reference = connectionReference.split(' [')[0].strip()
|
||||
|
||||
parts = base_reference.split(':')
|
||||
if len(parts) != 3 or parts[0] != "connection":
|
||||
return None
|
||||
|
||||
if len(parts) == 3 and parts[0] == "connection":
|
||||
authority = parts[1]
|
||||
username = parts[2]
|
||||
|
||||
# Get user connections through AppObjects interface
|
||||
user_connections = self.interfaceDbApp.getUserConnections(self.user.id)
|
||||
|
||||
# Find matching connection by authority and username (no UUID needed)
|
||||
for conn in user_connections:
|
||||
if conn.authority.value == authority and conn.externalUsername == username:
|
||||
connAuthority = conn.authority.value if hasattr(conn.authority, "value") else str(conn.authority)
|
||||
if connAuthority == authority and conn.externalUsername == username:
|
||||
return conn
|
||||
|
||||
# Fallback: treat the reference as a connection ID (UUID)
|
||||
user_connections = self.interfaceDbApp.getUserConnections(self.user.id)
|
||||
for conn in user_connections:
|
||||
connId = conn.get("id") if isinstance(conn, dict) else getattr(conn, "id", None)
|
||||
if connId and str(connId) == base_reference:
|
||||
return conn
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing connection reference: {str(e)}")
|
||||
logger.error(f"Error parsing connection reference '{connectionReference}': {str(e)}")
|
||||
return None
|
||||
|
||||
def getFreshConnectionToken(self, connectionId: str):
|
||||
|
|
|
|||
|
|
@ -146,12 +146,15 @@ class KnowledgeService:
|
|||
# 3. Chunk text content objects and create embeddings
|
||||
textObjects = [o for o in contentObjects if o.get("contentType") == "text"]
|
||||
|
||||
if _shouldNeutralize and textObjects:
|
||||
_neutralizedObjects = []
|
||||
_neutralSvc = None
|
||||
if _shouldNeutralize:
|
||||
try:
|
||||
_neutralSvc = self._getService("neutralization")
|
||||
except Exception:
|
||||
_neutralSvc = None
|
||||
logger.warning(f"Neutralization service unavailable for file {fileId}")
|
||||
|
||||
if _shouldNeutralize and textObjects:
|
||||
_neutralizedObjects = []
|
||||
if _neutralSvc:
|
||||
for _obj in textObjects:
|
||||
_textContent = (_obj.get("data", "") or "").strip()
|
||||
|
|
@ -201,7 +204,7 @@ class KnowledgeService:
|
|||
|
||||
# 4. Store non-text content objects (images, etc.) without embedding
|
||||
nonTextObjects = [o for o in contentObjects if o.get("contentType") != "text"]
|
||||
if _shouldNeutralize and nonTextObjects:
|
||||
if _shouldNeutralize and nonTextObjects and _neutralSvc:
|
||||
import base64 as _b64
|
||||
_filteredNonText = []
|
||||
for _obj in nonTextObjects:
|
||||
|
|
|
|||
|
|
@ -57,25 +57,43 @@ def getModelLabels(modelName: str, language: str = "en") -> Dict[str, str]:
|
|||
result: Dict[str, str] = {}
|
||||
for attr, translations in attributeLabels.items():
|
||||
if isinstance(translations, dict):
|
||||
result[attr] = translations.get(language, translations.get("en", attr))
|
||||
germanKey = translations.get("xx") or next(iter(translations.values()), attr)
|
||||
result[attr] = _resolveLabel(germanKey, language)
|
||||
elif isinstance(translations, str):
|
||||
result[attr] = _resolveLabel(translations, language)
|
||||
else:
|
||||
result[attr] = attr
|
||||
result[attr] = f"[{attr}]"
|
||||
return result
|
||||
|
||||
|
||||
def _resolveLabel(germanText: str, language: str) -> str:
|
||||
"""Resolve a German base label to the requested language via i18n cache."""
|
||||
"""Resolve a German base label to the requested language via i18n cache.
|
||||
Returns [germanText] when no translation exists so missing keys are visible in the UI."""
|
||||
if language == "de":
|
||||
return germanText
|
||||
try:
|
||||
from modules.shared.i18nRegistry import _CACHE
|
||||
return _CACHE.get(language, {}).get(germanText, germanText)
|
||||
return _CACHE.get(language, {}).get(germanText, f"[{germanText}]")
|
||||
except ImportError:
|
||||
return germanText
|
||||
|
||||
|
||||
def _resolveOptionLabels(options, userLanguage: str):
|
||||
"""Resolve frontend_options label values to the requested language."""
|
||||
if not isinstance(options, list):
|
||||
return options
|
||||
for opt in options:
|
||||
if not isinstance(opt, dict) or "label" not in opt:
|
||||
continue
|
||||
raw = opt["label"]
|
||||
if isinstance(raw, dict):
|
||||
germanKey = raw.get("xx") or next(iter(raw.values()), str(opt.get("value", "")))
|
||||
opt["label"] = _resolveLabel(germanKey, userLanguage)
|
||||
elif isinstance(raw, str):
|
||||
opt["label"] = _resolveLabel(raw, userLanguage)
|
||||
return options
|
||||
|
||||
|
||||
def _mergedAttributeLabels(modelClass: Type[BaseModel], userLanguage: str) -> Dict[str, str]:
|
||||
"""Merge attribute labels from model MRO (base classes first, subclass overrides)."""
|
||||
try:
|
||||
|
|
@ -88,15 +106,16 @@ def _mergedAttributeLabels(modelClass: Type[BaseModel], userLanguage: str) -> Di
|
|||
return merged
|
||||
|
||||
|
||||
def getModelLabel(modelName: str, language: str = "en") -> str:
|
||||
def getModelLabel(modelName: str, language: str = "de") -> str:
|
||||
"""Get the label for a model in the specified language (see getModelLabels)."""
|
||||
modelData = _getModelLabelEntry(modelName)
|
||||
modelLabel = modelData.get("model", {})
|
||||
if isinstance(modelLabel, dict):
|
||||
return modelLabel.get(language, modelLabel.get("en", modelName))
|
||||
germanKey = modelLabel.get("xx") or next(iter(modelLabel.values()), modelName)
|
||||
return _resolveLabel(germanKey, language)
|
||||
elif isinstance(modelLabel, str):
|
||||
return _resolveLabel(modelLabel, language)
|
||||
return modelName
|
||||
return f"[{modelName}]"
|
||||
|
||||
|
||||
def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguage: str = "en") -> Dict[str, Any]:
|
||||
|
|
@ -254,7 +273,7 @@ def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguag
|
|||
"visible": frontend_visible,
|
||||
"order": len(attributes),
|
||||
"readonly": frontend_readonly,
|
||||
"options": frontend_options,
|
||||
"options": _resolveOptionLabels(frontend_options, userLanguage),
|
||||
"default": field_default,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,16 @@ from pydantic import BaseModel
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _extractRegistrySourceText(obj: Any) -> str:
|
||||
"""Resolve a str or multilingual dict to one canonical registry key string."""
|
||||
if isinstance(obj, str):
|
||||
return obj
|
||||
if isinstance(obj, dict):
|
||||
return obj.get("xx") or next(iter(obj.values()), "") or ""
|
||||
return ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Registry (populated at import time by t() and @i18nModel)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -60,14 +70,14 @@ def t(key: str, context: str = "api", value: str = "") -> str:
|
|||
|
||||
At import time: registers the key with context and AI description.
|
||||
At runtime: returns the cached translation for _CURRENT_LANGUAGE.
|
||||
Falls back to the key itself (German base text) if no translation found.
|
||||
Falls back to [key] so missing translations are visible in the UI.
|
||||
"""
|
||||
if key not in _REGISTRY:
|
||||
_REGISTRY[key] = _I18nRegistryEntry(context=context, value=value)
|
||||
lang = _CURRENT_LANGUAGE.get()
|
||||
if lang == "de":
|
||||
return key
|
||||
return _CACHE.get(lang, {}).get(key, key)
|
||||
return _CACHE.get(lang, {}).get(key, f"[{key}]")
|
||||
|
||||
|
||||
def apiRouteContext(routeModuleName: str):
|
||||
|
|
@ -265,7 +275,7 @@ def _registerFeatureUiLabels():
|
|||
_REGISTRY[lab] = _I18nRegistryEntry(context="nav", value="")
|
||||
added += 1
|
||||
elif isinstance(lab, dict):
|
||||
base = lab.get("de") or lab.get("en")
|
||||
base = lab.get("xx") or next(iter(lab.values()), "")
|
||||
if base and base not in _REGISTRY:
|
||||
_REGISTRY[base] = _I18nRegistryEntry(context="nav", value="")
|
||||
added += 1
|
||||
|
|
@ -279,9 +289,9 @@ def _registerRbacLabels():
|
|||
context mapping:
|
||||
- DATA_OBJECTS → rbac.data
|
||||
- RESOURCE_OBJECTS → rbac.resource
|
||||
- TEMPLATE_ROLES[].description (de) → rbac.role
|
||||
- QUICK_ACTIONS[].label/description (de) → rbac.quickaction
|
||||
- QUICK_ACTION_CATEGORIES[].label (de) → rbac.quickaction
|
||||
- TEMPLATE_ROLES[].description (xx source) → rbac.role
|
||||
- QUICK_ACTIONS[].label/description (xx source) → rbac.quickaction
|
||||
- QUICK_ACTION_CATEGORIES[].label (xx source) → rbac.quickaction
|
||||
"""
|
||||
_systemModule = "modules.system.mainSystem"
|
||||
_featureModulePaths = (
|
||||
|
|
@ -296,13 +306,6 @@ def _registerRbacLabels():
|
|||
"modules.features.chatbot.mainChatbot",
|
||||
)
|
||||
|
||||
def _extractDe(obj) -> str:
|
||||
if isinstance(obj, str):
|
||||
return obj
|
||||
if isinstance(obj, dict):
|
||||
return obj.get("de") or obj.get("en") or ""
|
||||
return ""
|
||||
|
||||
added = 0
|
||||
for modPath in _featureModulePaths:
|
||||
try:
|
||||
|
|
@ -314,32 +317,32 @@ def _registerRbacLabels():
|
|||
continue
|
||||
|
||||
for dataObj in getattr(mod, "DATA_OBJECTS", []) or []:
|
||||
key = _extractDe(dataObj.get("label"))
|
||||
key = _extractRegistrySourceText(dataObj.get("label"))
|
||||
if key and key not in _REGISTRY:
|
||||
_REGISTRY[key] = _I18nRegistryEntry(context="rbac.data", value="")
|
||||
added += 1
|
||||
|
||||
for resObj in getattr(mod, "RESOURCE_OBJECTS", []) or []:
|
||||
key = _extractDe(resObj.get("label"))
|
||||
key = _extractRegistrySourceText(resObj.get("label"))
|
||||
if key and key not in _REGISTRY:
|
||||
_REGISTRY[key] = _I18nRegistryEntry(context="rbac.resource", value="")
|
||||
added += 1
|
||||
|
||||
for role in getattr(mod, "TEMPLATE_ROLES", []) or []:
|
||||
key = _extractDe(role.get("description"))
|
||||
key = _extractRegistrySourceText(role.get("description"))
|
||||
if key and key not in _REGISTRY:
|
||||
_REGISTRY[key] = _I18nRegistryEntry(context="rbac.role", value="")
|
||||
added += 1
|
||||
|
||||
for qa in getattr(mod, "QUICK_ACTIONS", []) or []:
|
||||
for field in ("label", "description"):
|
||||
key = _extractDe(qa.get(field))
|
||||
key = _extractRegistrySourceText(qa.get(field))
|
||||
if key and key not in _REGISTRY:
|
||||
_REGISTRY[key] = _I18nRegistryEntry(context="rbac.quickaction", value="")
|
||||
added += 1
|
||||
|
||||
for cat in getattr(mod, "QUICK_ACTION_CATEGORIES", []) or []:
|
||||
key = _extractDe(cat.get("label"))
|
||||
key = _extractRegistrySourceText(cat.get("label"))
|
||||
if key and key not in _REGISTRY:
|
||||
_REGISTRY[key] = _I18nRegistryEntry(context="rbac.quickaction", value="")
|
||||
added += 1
|
||||
|
|
@ -351,17 +354,10 @@ def _registerServiceCenterLabels():
|
|||
"""Register service-center category labels and bootstrap role descriptions."""
|
||||
added = 0
|
||||
|
||||
def _extractDe(obj) -> str:
|
||||
if isinstance(obj, str):
|
||||
return obj
|
||||
if isinstance(obj, dict):
|
||||
return obj.get("de") or obj.get("en") or ""
|
||||
return ""
|
||||
|
||||
try:
|
||||
from modules.serviceCenter.registry import IMPORTABLE_SERVICES
|
||||
for svc in IMPORTABLE_SERVICES.values():
|
||||
key = _extractDe(svc.get("label"))
|
||||
key = _extractRegistrySourceText(svc.get("label"))
|
||||
if key and key not in _REGISTRY:
|
||||
_REGISTRY[key] = _I18nRegistryEntry(context="service", value="")
|
||||
added += 1
|
||||
|
|
@ -387,13 +383,6 @@ def _registerNodeLabels():
|
|||
output labels, port descriptions, category labels, and entry-point titles."""
|
||||
added = 0
|
||||
|
||||
def _extractDe(obj) -> str:
|
||||
if isinstance(obj, str):
|
||||
return obj
|
||||
if isinstance(obj, dict):
|
||||
return obj.get("de") or obj.get("en") or ""
|
||||
return ""
|
||||
|
||||
def _reg(key: str, ctx: str):
|
||||
nonlocal added
|
||||
if key and key not in _REGISTRY:
|
||||
|
|
@ -403,17 +392,19 @@ def _registerNodeLabels():
|
|||
try:
|
||||
from modules.features.graphicalEditor.nodeDefinitions import STATIC_NODE_TYPES
|
||||
for nd in STATIC_NODE_TYPES:
|
||||
_reg(_extractDe(nd.get("label")), "node.label")
|
||||
_reg(_extractDe(nd.get("description")), "node.desc")
|
||||
_reg(_extractRegistrySourceText(nd.get("label")), "node.label")
|
||||
_reg(_extractRegistrySourceText(nd.get("description")), "node.desc")
|
||||
|
||||
for param in nd.get("parameters", []) or []:
|
||||
_reg(_extractDe(param.get("description")), "node.param")
|
||||
_reg(_extractDe(param.get("label")), "node.param")
|
||||
_reg(_extractRegistrySourceText(param.get("description")), "node.param")
|
||||
_reg(_extractRegistrySourceText(param.get("label")), "node.param")
|
||||
|
||||
outLabels = nd.get("outputLabels")
|
||||
if isinstance(outLabels, dict):
|
||||
deList = outLabels.get("de") or outLabels.get("en") or []
|
||||
for lbl in deList:
|
||||
sourceList = outLabels.get("xx") or next(iter(outLabels.values()), [])
|
||||
if not isinstance(sourceList, list):
|
||||
sourceList = []
|
||||
for lbl in sourceList:
|
||||
_reg(lbl, "node.output")
|
||||
elif isinstance(outLabels, list):
|
||||
for lbl in outLabels:
|
||||
|
|
@ -427,7 +418,7 @@ def _registerNodeLabels():
|
|||
for field in getattr(schema, "fields", []) or []:
|
||||
desc = getattr(field, "description", None)
|
||||
if desc:
|
||||
_reg(_extractDe(desc if isinstance(desc, (str, dict)) else None), "port.desc")
|
||||
_reg(_extractRegistrySourceText(desc if isinstance(desc, (str, dict)) else None), "port.desc")
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
|
@ -449,13 +440,6 @@ def _registerDatamodelOptionLabels():
|
|||
"""Register all frontend_options labels from Pydantic datamodels and subscription plans."""
|
||||
added = 0
|
||||
|
||||
def _extractDe(obj) -> str:
|
||||
if isinstance(obj, str):
|
||||
return obj
|
||||
if isinstance(obj, dict):
|
||||
return obj.get("de") or obj.get("en") or ""
|
||||
return ""
|
||||
|
||||
def _reg(key: str, ctx: str):
|
||||
nonlocal added
|
||||
if key and key not in _REGISTRY:
|
||||
|
|
@ -495,13 +479,13 @@ def _registerDatamodelOptionLabels():
|
|||
ctx = f"option.{cls.__name__}.{fieldName}"
|
||||
for opt in options:
|
||||
if isinstance(opt, dict):
|
||||
_reg(_extractDe(opt.get("label")), ctx)
|
||||
_reg(_extractRegistrySourceText(opt.get("label")), ctx)
|
||||
|
||||
try:
|
||||
from modules.datamodels.datamodelSubscription import BUILTIN_PLANS
|
||||
for plan in BUILTIN_PLANS.values():
|
||||
_reg(_extractDe(getattr(plan, "title", None)), "subscription.title")
|
||||
_reg(_extractDe(getattr(plan, "description", None)), "subscription.desc")
|
||||
_reg(_extractRegistrySourceText(getattr(plan, "title", None)), "subscription.title")
|
||||
_reg(_extractRegistrySourceText(getattr(plan, "description", None)), "subscription.desc")
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Starts/stops cron jobs for workflows with schedule entry points.
|
|||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from modules.shared.eventManagement import eventManager
|
||||
|
||||
|
|
@ -213,11 +213,14 @@ def _create_schedule_handler(
|
|||
discoverMethods(services)
|
||||
|
||||
title = (inv or {}).get("title") or {}
|
||||
label = ""
|
||||
requestLang: Optional[str] = getattr(event_user, "language", None)
|
||||
if isinstance(title, dict):
|
||||
label = title.get("en") or title.get("de") or ""
|
||||
picked = (requestLang and title.get(requestLang)) or title.get("xx") or next(iter(title.values()), "")
|
||||
label = str(picked) if picked else ""
|
||||
elif isinstance(title, str):
|
||||
label = title
|
||||
else:
|
||||
label = ""
|
||||
|
||||
run_env = default_run_envelope(
|
||||
"schedule",
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ Replaces subAutomation2Schedule with v1-style incremental sync patterns:
|
|||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from modules.shared.eventManagement import eventManager
|
||||
|
||||
|
|
@ -231,11 +231,14 @@ class WorkflowScheduler:
|
|||
discoverMethods(services)
|
||||
|
||||
title = (inv or {}).get("title") or {}
|
||||
label = ""
|
||||
requestLang: Optional[str] = getattr(eventUser, "language", None)
|
||||
if isinstance(title, dict):
|
||||
label = title.get("en") or title.get("de") or ""
|
||||
picked = (requestLang and title.get(requestLang)) or title.get("xx") or next(iter(title.values()), "")
|
||||
label = str(picked) if picked else ""
|
||||
elif isinstance(title, str):
|
||||
label = title
|
||||
else:
|
||||
label = ""
|
||||
|
||||
runEnv = default_run_envelope(
|
||||
"schedule",
|
||||
|
|
|
|||
Loading…
Reference in a new issue