fixes in node languages and ai workflow
This commit is contained in:
parent
17455688a9
commit
61f04a6049
29 changed files with 1016 additions and 297 deletions
|
|
@ -76,8 +76,8 @@ class InvestorDemo2026(_BaseDemoConfig):
|
|||
self._ensureTrusteeRmaConfig(db, mandateIdHappy, _MANDATE_HAPPYLIFE["label"], summary)
|
||||
self._ensureTrusteeRmaConfig(db, mandateIdAlpina, _MANDATE_ALPINA["label"], summary)
|
||||
|
||||
self._ensureNeutralizationConfig(db, mandateIdHappy, summary)
|
||||
self._ensureNeutralizationConfig(db, mandateIdAlpina, summary)
|
||||
self._ensureNeutralizationConfig(db, mandateIdHappy, userId, summary)
|
||||
self._ensureNeutralizationConfig(db, mandateIdAlpina, userId, summary)
|
||||
|
||||
self._ensureBilling(db, mandateIdHappy, _MANDATE_HAPPYLIFE["label"], summary)
|
||||
self._ensureBilling(db, mandateIdAlpina, _MANDATE_ALPINA["label"], summary)
|
||||
|
|
@ -179,7 +179,8 @@ class InvestorDemo2026(_BaseDemoConfig):
|
|||
return uid
|
||||
|
||||
def _ensureMembership(self, db, userId: str, mandateId: str, mandateLabel: str, summary: Dict):
|
||||
from modules.datamodels.datamodelMembership import UserMandate, UserMandateRole, Role
|
||||
from modules.datamodels.datamodelMembership import UserMandate, UserMandateRole
|
||||
from modules.datamodels.datamodelRbac import Role
|
||||
|
||||
existing = db.getRecordset(UserMandate, recordFilter={"userId": userId, "mandateId": mandateId})
|
||||
if existing:
|
||||
|
|
@ -192,7 +193,7 @@ class InvestorDemo2026(_BaseDemoConfig):
|
|||
summary["created"].append(f"Membership {_USER['username']} -> {mandateLabel}")
|
||||
logger.info(f"Created membership {_USER['username']} -> {mandateLabel}")
|
||||
|
||||
adminRoles = db.getRecordset(Role, recordFilter={"mandateId": mandateId, "label": "admin"})
|
||||
adminRoles = db.getRecordset(Role, recordFilter={"mandateId": mandateId, "roleLabel": "admin"})
|
||||
if adminRoles:
|
||||
adminRoleId = adminRoles[0].get("id")
|
||||
existingRole = db.getRecordset(UserMandateRole, recordFilter={"userMandateId": userMandateId, "roleId": adminRoleId})
|
||||
|
|
@ -205,7 +206,7 @@ class InvestorDemo2026(_BaseDemoConfig):
|
|||
from modules.interfaces.interfaceFeatures import getFeatureInterface
|
||||
|
||||
fi = getFeatureInterface(db)
|
||||
existingInstances = fi.getFeatureInstances(mandateId)
|
||||
existingInstances = fi.getFeatureInstancesForMandate(mandateId)
|
||||
existingCodes = {
|
||||
(inst.featureCode if hasattr(inst, "featureCode") else inst.get("featureCode", ""))
|
||||
for inst in existingInstances
|
||||
|
|
@ -278,8 +279,8 @@ class InvestorDemo2026(_BaseDemoConfig):
|
|||
summary["created"].append(f"RMA accounting config for {mandateLabel}")
|
||||
logger.info(f"Created RMA accounting config for {mandateLabel}")
|
||||
|
||||
def _ensureNeutralizationConfig(self, db, mandateId: Optional[str], summary: Dict):
|
||||
if not mandateId:
|
||||
def _ensureNeutralizationConfig(self, db, mandateId: Optional[str], userId: Optional[str], summary: Dict):
|
||||
if not mandateId or not userId:
|
||||
return
|
||||
|
||||
from modules.datamodels.datamodelFeatures import FeatureInstance
|
||||
|
|
@ -301,6 +302,7 @@ class InvestorDemo2026(_BaseDemoConfig):
|
|||
config = DataNeutraliserConfig(
|
||||
featureInstanceId=instanceId,
|
||||
mandateId=mandateId,
|
||||
userId=userId,
|
||||
enabled=True,
|
||||
scope="featureInstance",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# AI node definitions - map to methodAi actions.
|
||||
|
||||
from modules.shared.i18nRegistry import t
|
||||
|
||||
AI_NODES = [
|
||||
{
|
||||
"id": "ai.prompt",
|
||||
"category": "ai",
|
||||
"label": "Prompt",
|
||||
"description": "Prompt eingeben und KI führt aus",
|
||||
"label": t("Prompt"),
|
||||
"description": t("Prompt eingeben und KI führt aus"),
|
||||
"parameters": [
|
||||
{"name": "aiPrompt", "type": "string", "required": True, "frontendType": "textarea",
|
||||
"description": "KI-Prompt"},
|
||||
"description": t("KI-Prompt")},
|
||||
{"name": "outputFormat", "type": "string", "required": False, "frontendType": "select",
|
||||
"frontendOptions": {"options": ["text", "json", "emailDraft"]},
|
||||
"description": "Ausgabeformat", "default": "text"},
|
||||
"description": t("Ausgabeformat"), "default": "text"},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -25,11 +27,11 @@ AI_NODES = [
|
|||
{
|
||||
"id": "ai.webResearch",
|
||||
"category": "ai",
|
||||
"label": "Web-Recherche",
|
||||
"description": "Recherche im Web",
|
||||
"label": t("Web-Recherche"),
|
||||
"description": t("Recherche im Web"),
|
||||
"parameters": [
|
||||
{"name": "prompt", "type": "string", "required": True, "frontendType": "textarea",
|
||||
"description": "Recherche-Anfrage"},
|
||||
"description": t("Recherche-Anfrage")},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -42,12 +44,12 @@ AI_NODES = [
|
|||
{
|
||||
"id": "ai.summarizeDocument",
|
||||
"category": "ai",
|
||||
"label": "Dokument zusammenfassen",
|
||||
"description": "Dokumentinhalt zusammenfassen",
|
||||
"label": t("Dokument zusammenfassen"),
|
||||
"description": t("Dokumentinhalt zusammenfassen"),
|
||||
"parameters": [
|
||||
{"name": "summaryLength", "type": "string", "required": False, "frontendType": "select",
|
||||
"frontendOptions": {"options": ["short", "medium", "long"]},
|
||||
"description": "Kurz, mittel oder lang", "default": "medium"},
|
||||
"description": t("Kurz, mittel oder lang"), "default": "medium"},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -60,12 +62,12 @@ AI_NODES = [
|
|||
{
|
||||
"id": "ai.translateDocument",
|
||||
"category": "ai",
|
||||
"label": "Dokument übersetzen",
|
||||
"description": "Dokument in Zielsprache übersetzen",
|
||||
"label": t("Dokument übersetzen"),
|
||||
"description": t("Dokument in Zielsprache übersetzen"),
|
||||
"parameters": [
|
||||
{"name": "targetLanguage", "type": "string", "required": True, "frontendType": "select",
|
||||
"frontendOptions": {"options": ["en", "de", "fr", "it", "es", "pt", "nl"]},
|
||||
"description": "Zielsprache"},
|
||||
"description": t("Zielsprache")},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -78,12 +80,12 @@ AI_NODES = [
|
|||
{
|
||||
"id": "ai.convertDocument",
|
||||
"category": "ai",
|
||||
"label": "Dokument konvertieren",
|
||||
"description": "Dokument in anderes Format konvertieren",
|
||||
"label": t("Dokument konvertieren"),
|
||||
"description": t("Dokument in anderes Format konvertieren"),
|
||||
"parameters": [
|
||||
{"name": "targetFormat", "type": "string", "required": True, "frontendType": "select",
|
||||
"frontendOptions": {"options": ["pdf", "docx", "txt", "html", "md"]},
|
||||
"description": "Zielformat"},
|
||||
"description": t("Zielformat")},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -96,11 +98,11 @@ AI_NODES = [
|
|||
{
|
||||
"id": "ai.generateDocument",
|
||||
"category": "ai",
|
||||
"label": "Dokument generieren",
|
||||
"description": "Dokument aus Prompt generieren",
|
||||
"label": t("Dokument generieren"),
|
||||
"description": t("Dokument aus Prompt generieren"),
|
||||
"parameters": [
|
||||
{"name": "prompt", "type": "string", "required": True, "frontendType": "textarea",
|
||||
"description": "Generierungs-Prompt"},
|
||||
"description": t("Generierungs-Prompt")},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -113,14 +115,14 @@ AI_NODES = [
|
|||
{
|
||||
"id": "ai.generateCode",
|
||||
"category": "ai",
|
||||
"label": "Code generieren",
|
||||
"description": "Code aus Beschreibung generieren",
|
||||
"label": t("Code generieren"),
|
||||
"description": t("Code aus Beschreibung generieren"),
|
||||
"parameters": [
|
||||
{"name": "prompt", "type": "string", "required": True, "frontendType": "textarea",
|
||||
"description": "Code-Generierungs-Prompt"},
|
||||
"description": t("Code-Generierungs-Prompt")},
|
||||
{"name": "language", "type": "string", "required": False, "frontendType": "select",
|
||||
"frontendOptions": {"options": ["python", "javascript", "typescript", "java", "csharp", "go"]},
|
||||
"description": "Programmiersprache", "default": "python"},
|
||||
"description": t("Programmiersprache"), "default": "python"},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
|
|||
|
|
@ -2,30 +2,32 @@
|
|||
# All rights reserved.
|
||||
"""ClickUp nodes — map to MethodClickup actions."""
|
||||
|
||||
from modules.shared.i18nRegistry import t
|
||||
|
||||
CLICKUP_NODES = [
|
||||
{
|
||||
"id": "clickup.searchTasks",
|
||||
"category": "clickup",
|
||||
"label": "Aufgaben suchen",
|
||||
"description": "Aufgaben in einem Workspace suchen",
|
||||
"label": t("Aufgaben suchen"),
|
||||
"description": t("Aufgaben in einem Workspace suchen"),
|
||||
"parameters": [
|
||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||
"description": "ClickUp-Verbindung"},
|
||||
"description": t("ClickUp-Verbindung")},
|
||||
{"name": "teamId", "type": "string", "required": True, "frontendType": "text",
|
||||
"description": "Team-/Workspace-ID"},
|
||||
"description": t("Team-/Workspace-ID")},
|
||||
{"name": "query", "type": "string", "required": True, "frontendType": "text",
|
||||
"description": "Suchbegriff"},
|
||||
"description": t("Suchbegriff")},
|
||||
{"name": "page", "type": "number", "required": False, "frontendType": "number",
|
||||
"description": "Seite", "default": 0},
|
||||
"description": t("Seite"), "default": 0},
|
||||
{"name": "listId", "type": "string", "required": False, "frontendType": "clickupList",
|
||||
"frontendOptions": {"dependsOn": "connectionReference"},
|
||||
"description": "In dieser Liste suchen"},
|
||||
"description": t("In dieser Liste suchen")},
|
||||
{"name": "includeClosed", "type": "boolean", "required": False, "frontendType": "checkbox",
|
||||
"description": "Erledigte einbeziehen", "default": False},
|
||||
"description": t("Erledigte einbeziehen"), "default": False},
|
||||
{"name": "fullTaskData", "type": "boolean", "required": False, "frontendType": "checkbox",
|
||||
"description": "Vollständige Daten", "default": False},
|
||||
"description": t("Vollständige Daten"), "default": False},
|
||||
{"name": "matchNameOnly", "type": "boolean", "required": False, "frontendType": "checkbox",
|
||||
"description": "Nur Titel", "default": True},
|
||||
"description": t("Nur Titel"), "default": True},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -38,18 +40,18 @@ CLICKUP_NODES = [
|
|||
{
|
||||
"id": "clickup.listTasks",
|
||||
"category": "clickup",
|
||||
"label": "Aufgaben auflisten",
|
||||
"description": "Aufgaben einer Liste auflisten",
|
||||
"label": t("Aufgaben auflisten"),
|
||||
"description": t("Aufgaben einer Liste auflisten"),
|
||||
"parameters": [
|
||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||
"description": "ClickUp-Verbindung"},
|
||||
"description": t("ClickUp-Verbindung")},
|
||||
{"name": "pathQuery", "type": "string", "required": True, "frontendType": "clickupList",
|
||||
"frontendOptions": {"dependsOn": "connectionReference"},
|
||||
"description": "Pfad zur Liste"},
|
||||
"description": t("Pfad zur Liste")},
|
||||
{"name": "page", "type": "number", "required": False, "frontendType": "number",
|
||||
"description": "Seite", "default": 0},
|
||||
"description": t("Seite"), "default": 0},
|
||||
{"name": "includeClosed", "type": "boolean", "required": False, "frontendType": "checkbox",
|
||||
"description": "Erledigte einbeziehen", "default": False},
|
||||
"description": t("Erledigte einbeziehen"), "default": False},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -62,15 +64,15 @@ CLICKUP_NODES = [
|
|||
{
|
||||
"id": "clickup.getTask",
|
||||
"category": "clickup",
|
||||
"label": "Aufgabe abrufen",
|
||||
"description": "Eine Aufgabe abrufen",
|
||||
"label": t("Aufgabe abrufen"),
|
||||
"description": t("Eine Aufgabe abrufen"),
|
||||
"parameters": [
|
||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||
"description": "ClickUp-Verbindung"},
|
||||
"description": t("ClickUp-Verbindung")},
|
||||
{"name": "taskId", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Task-ID"},
|
||||
"description": t("Task-ID")},
|
||||
{"name": "pathQuery", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Oder Pfad"},
|
||||
"description": t("Oder Pfad")},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -83,39 +85,39 @@ CLICKUP_NODES = [
|
|||
{
|
||||
"id": "clickup.createTask",
|
||||
"category": "clickup",
|
||||
"label": "Aufgabe erstellen",
|
||||
"description": "Aufgabe erstellen",
|
||||
"label": t("Aufgabe erstellen"),
|
||||
"description": t("Aufgabe erstellen"),
|
||||
"parameters": [
|
||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||
"description": "ClickUp-Verbindung"},
|
||||
"description": t("ClickUp-Verbindung")},
|
||||
{"name": "teamId", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Workspace"},
|
||||
"description": t("Workspace")},
|
||||
{"name": "pathQuery", "type": "string", "required": False, "frontendType": "clickupList",
|
||||
"frontendOptions": {"dependsOn": "connectionReference"},
|
||||
"description": "Pfad zur Liste"},
|
||||
"description": t("Pfad zur Liste")},
|
||||
{"name": "listId", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Listen-ID"},
|
||||
"description": t("Listen-ID")},
|
||||
{"name": "name", "type": "string", "required": True, "frontendType": "text",
|
||||
"description": "Name"},
|
||||
"description": t("Name")},
|
||||
{"name": "description", "type": "string", "required": False, "frontendType": "textarea",
|
||||
"description": "Beschreibung"},
|
||||
"description": t("Beschreibung")},
|
||||
{"name": "taskStatus", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Status"},
|
||||
"description": t("Status")},
|
||||
{"name": "taskPriority", "type": "string", "required": False, "frontendType": "select",
|
||||
"frontendOptions": {"options": ["1", "2", "3", "4"]},
|
||||
"description": "Priorität 1-4"},
|
||||
"description": t("Priorität 1-4")},
|
||||
{"name": "taskDueDateMs", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Fälligkeit (ms)"},
|
||||
"description": t("Fälligkeit (ms)")},
|
||||
{"name": "taskAssigneeIds", "type": "object", "required": False, "frontendType": "json",
|
||||
"description": "Zugewiesene"},
|
||||
"description": t("Zugewiesene")},
|
||||
{"name": "taskTimeEstimateMs", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Zeitschätzung (ms)"},
|
||||
"description": t("Zeitschätzung (ms)")},
|
||||
{"name": "taskTimeEstimateHours", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Zeitschätzung (h)"},
|
||||
"description": t("Zeitschätzung (h)")},
|
||||
{"name": "customFieldValues", "type": "object", "required": False, "frontendType": "json",
|
||||
"description": "Benutzerdefinierte Felder"},
|
||||
"description": t("Benutzerdefinierte Felder")},
|
||||
{"name": "taskFields", "type": "string", "required": False, "frontendType": "json",
|
||||
"description": "Zusätzliches JSON"},
|
||||
"description": t("Zusätzliches JSON")},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -128,19 +130,19 @@ CLICKUP_NODES = [
|
|||
{
|
||||
"id": "clickup.updateTask",
|
||||
"category": "clickup",
|
||||
"label": "Aufgabe aktualisieren",
|
||||
"description": "Felder der Aufgabe ändern",
|
||||
"label": t("Aufgabe aktualisieren"),
|
||||
"description": t("Felder der Aufgabe ändern"),
|
||||
"parameters": [
|
||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||
"description": "ClickUp-Verbindung"},
|
||||
"description": t("ClickUp-Verbindung")},
|
||||
{"name": "taskId", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Task-ID"},
|
||||
"description": t("Task-ID")},
|
||||
{"name": "path", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Oder Pfad"},
|
||||
"description": t("Oder Pfad")},
|
||||
{"name": "taskUpdateEntries", "type": "object", "required": False, "frontendType": "keyValueRows",
|
||||
"description": "Zu ändernde Felder"},
|
||||
"description": t("Zu ändernde Felder")},
|
||||
{"name": "taskUpdate", "type": "string", "required": False, "frontendType": "json",
|
||||
"description": "JSON für API"},
|
||||
"description": t("JSON für API")},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -153,17 +155,17 @@ CLICKUP_NODES = [
|
|||
{
|
||||
"id": "clickup.uploadAttachment",
|
||||
"category": "clickup",
|
||||
"label": "Anhang hochladen",
|
||||
"description": "Datei an Task anhängen",
|
||||
"label": t("Anhang hochladen"),
|
||||
"description": t("Datei an Task anhängen"),
|
||||
"parameters": [
|
||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||
"description": "ClickUp-Verbindung"},
|
||||
"description": t("ClickUp-Verbindung")},
|
||||
{"name": "taskId", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Task-ID"},
|
||||
"description": t("Task-ID")},
|
||||
{"name": "path", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Oder Pfad"},
|
||||
"description": t("Oder Pfad")},
|
||||
{"name": "fileName", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Dateiname"},
|
||||
"description": t("Dateiname")},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Data manipulation node definitions: aggregate, transform, filter.
|
||||
|
||||
from modules.shared.i18nRegistry import t
|
||||
|
||||
DATA_NODES = [
|
||||
{
|
||||
"id": "data.aggregate",
|
||||
"category": "data",
|
||||
"label": "Sammeln",
|
||||
"description": "Ergebnisse aus Schleifen-Iterationen sammeln",
|
||||
"label": t("Sammeln"),
|
||||
"description": t("Ergebnisse aus Schleifen-Iterationen sammeln"),
|
||||
"parameters": [
|
||||
{"name": "mode", "type": "string", "required": False, "frontendType": "select",
|
||||
"frontendOptions": {"options": ["collect", "concat", "sum", "count"]},
|
||||
"description": "Aggregationsmodus", "default": "collect"},
|
||||
"description": t("Aggregationsmodus"), "default": "collect"},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -22,11 +24,11 @@ DATA_NODES = [
|
|||
{
|
||||
"id": "data.transform",
|
||||
"category": "data",
|
||||
"label": "Umwandeln",
|
||||
"description": "Daten umstrukturieren",
|
||||
"label": t("Umwandeln"),
|
||||
"description": t("Daten umstrukturieren"),
|
||||
"parameters": [
|
||||
{"name": "mappings", "type": "json", "required": True, "frontendType": "mappingTable",
|
||||
"description": "Feld-Zuordnungen", "default": []},
|
||||
"description": t("Feld-Zuordnungen"), "default": []},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -38,11 +40,11 @@ DATA_NODES = [
|
|||
{
|
||||
"id": "data.filter",
|
||||
"category": "data",
|
||||
"label": "Filtern",
|
||||
"description": "Elemente nach Bedingung filtern",
|
||||
"label": t("Filtern"),
|
||||
"description": t("Elemente nach Bedingung filtern"),
|
||||
"parameters": [
|
||||
{"name": "condition", "type": "string", "required": True, "frontendType": "filterExpression",
|
||||
"description": "Filterbedingung"},
|
||||
"description": t("Filterbedingung")},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
|
|||
|
|
@ -1,27 +1,29 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Email node definitions - map to methodOutlook actions.
|
||||
|
||||
from modules.shared.i18nRegistry import t
|
||||
|
||||
EMAIL_NODES = [
|
||||
{
|
||||
"id": "email.checkEmail",
|
||||
"category": "email",
|
||||
"label": "E-Mail prüfen",
|
||||
"description": "Neue E-Mails prüfen",
|
||||
"label": t("E-Mail prüfen"),
|
||||
"description": t("Neue E-Mails prüfen"),
|
||||
"parameters": [
|
||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||
"description": "E-Mail-Konto Verbindung"},
|
||||
"description": t("E-Mail-Konto Verbindung")},
|
||||
{"name": "folder", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Ordner", "default": "Inbox"},
|
||||
"description": t("Ordner"), "default": "Inbox"},
|
||||
{"name": "limit", "type": "number", "required": False, "frontendType": "number",
|
||||
"description": "Max E-Mails", "default": 100},
|
||||
"description": t("Max E-Mails"), "default": 100},
|
||||
{"name": "fromAddress", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Nur von dieser Adresse", "default": ""},
|
||||
"description": t("Nur von dieser Adresse"), "default": ""},
|
||||
{"name": "subjectContains", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Betreff muss enthalten", "default": ""},
|
||||
"description": t("Betreff muss enthalten"), "default": ""},
|
||||
{"name": "hasAttachment", "type": "boolean", "required": False, "frontendType": "checkbox",
|
||||
"description": "Nur mit Anhängen", "default": False},
|
||||
"description": t("Nur mit Anhängen"), "default": False},
|
||||
{"name": "filter", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Erweitert: Filter-Text", "default": ""},
|
||||
"description": t("Erweitert: Filter-Text"), "default": ""},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -34,29 +36,29 @@ EMAIL_NODES = [
|
|||
{
|
||||
"id": "email.searchEmail",
|
||||
"category": "email",
|
||||
"label": "E-Mail suchen",
|
||||
"description": "E-Mails suchen",
|
||||
"label": t("E-Mail suchen"),
|
||||
"description": t("E-Mails suchen"),
|
||||
"parameters": [
|
||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||
"description": "E-Mail-Konto Verbindung"},
|
||||
"description": t("E-Mail-Konto Verbindung")},
|
||||
{"name": "query", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Suchbegriff", "default": ""},
|
||||
"description": t("Suchbegriff"), "default": ""},
|
||||
{"name": "folder", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Ordner", "default": "Inbox"},
|
||||
"description": t("Ordner"), "default": "Inbox"},
|
||||
{"name": "limit", "type": "number", "required": False, "frontendType": "number",
|
||||
"description": "Max E-Mails", "default": 100},
|
||||
"description": t("Max E-Mails"), "default": 100},
|
||||
{"name": "fromAddress", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Von Adresse", "default": ""},
|
||||
"description": t("Von Adresse"), "default": ""},
|
||||
{"name": "toAddress", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "An Adresse", "default": ""},
|
||||
"description": t("An Adresse"), "default": ""},
|
||||
{"name": "subjectContains", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Betreff enthält", "default": ""},
|
||||
"description": t("Betreff enthält"), "default": ""},
|
||||
{"name": "bodyContains", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Inhalt enthält", "default": ""},
|
||||
"description": t("Inhalt enthält"), "default": ""},
|
||||
{"name": "hasAttachment", "type": "boolean", "required": False, "frontendType": "checkbox",
|
||||
"description": "Mit Anhängen", "default": False},
|
||||
"description": t("Mit Anhängen"), "default": False},
|
||||
{"name": "filter", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Erweitert: KQL-Filter", "default": ""},
|
||||
"description": t("Erweitert: KQL-Filter"), "default": ""},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -69,17 +71,17 @@ EMAIL_NODES = [
|
|||
{
|
||||
"id": "email.draftEmail",
|
||||
"category": "email",
|
||||
"label": "E-Mail entwerfen",
|
||||
"description": "E-Mail-Entwurf erstellen",
|
||||
"label": t("E-Mail entwerfen"),
|
||||
"description": t("E-Mail-Entwurf erstellen"),
|
||||
"parameters": [
|
||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||
"description": "E-Mail-Konto"},
|
||||
"description": t("E-Mail-Konto")},
|
||||
{"name": "subject", "type": "string", "required": True, "frontendType": "text",
|
||||
"description": "Betreff"},
|
||||
"description": t("Betreff")},
|
||||
{"name": "body", "type": "string", "required": True, "frontendType": "textarea",
|
||||
"description": "Inhalt"},
|
||||
"description": t("Inhalt")},
|
||||
{"name": "to", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Empfänger", "default": ""},
|
||||
"description": t("Empfänger"), "default": ""},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
|
|||
|
|
@ -1,26 +1,28 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# File node definitions - create files from context (e.g. from AI nodes).
|
||||
|
||||
from modules.shared.i18nRegistry import t
|
||||
|
||||
FILE_NODES = [
|
||||
{
|
||||
"id": "file.create",
|
||||
"category": "file",
|
||||
"label": "Datei erstellen",
|
||||
"description": "Erstellt eine Datei aus Kontext (Text/Markdown von KI).",
|
||||
"label": t("Datei erstellen"),
|
||||
"description": t("Erstellt eine Datei aus Kontext (Text/Markdown von KI)."),
|
||||
"parameters": [
|
||||
{"name": "contentSources", "type": "json", "required": False, "frontendType": "json",
|
||||
"description": "Kontext-Quellen", "default": []},
|
||||
"description": t("Kontext-Quellen"), "default": []},
|
||||
{"name": "outputFormat", "type": "string", "required": True, "frontendType": "select",
|
||||
"frontendOptions": {"options": ["docx", "pdf", "txt", "html", "md"]},
|
||||
"description": "Ausgabeformat", "default": "docx"},
|
||||
"description": t("Ausgabeformat"), "default": "docx"},
|
||||
{"name": "title", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Dokumenttitel"},
|
||||
"description": t("Dokumenttitel")},
|
||||
{"name": "templateName", "type": "string", "required": False, "frontendType": "select",
|
||||
"frontendOptions": {"options": ["default", "corporate", "minimal"]},
|
||||
"description": "Stil-Vorlage"},
|
||||
"description": t("Stil-Vorlage")},
|
||||
{"name": "language", "type": "string", "required": False, "frontendType": "select",
|
||||
"frontendOptions": {"options": ["de", "en", "fr"]},
|
||||
"description": "Sprache", "default": "de"},
|
||||
"description": t("Sprache"), "default": "de"},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
|
|||
|
|
@ -1,24 +1,26 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Flow control node definitions.
|
||||
|
||||
from modules.shared.i18nRegistry import t
|
||||
|
||||
FLOW_NODES = [
|
||||
{
|
||||
"id": "flow.ifElse",
|
||||
"category": "flow",
|
||||
"label": "Wenn / Sonst",
|
||||
"description": "Verzweigung nach Bedingung",
|
||||
"label": t("Wenn / Sonst"),
|
||||
"description": t("Verzweigung nach Bedingung"),
|
||||
"parameters": [
|
||||
{
|
||||
"name": "condition",
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"frontendType": "condition",
|
||||
"description": "Bedingung",
|
||||
"description": t("Bedingung"),
|
||||
},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 2,
|
||||
"outputLabels": ["Ja", "Nein"],
|
||||
"outputLabels": [t("Ja"), t("Nein")],
|
||||
"inputPorts": {0: {"accepts": ["Transit"]}},
|
||||
"outputPorts": {0: {"schema": "Transit"}, 1: {"schema": "Transit"}},
|
||||
"executor": "flow",
|
||||
|
|
@ -27,22 +29,22 @@ FLOW_NODES = [
|
|||
{
|
||||
"id": "flow.switch",
|
||||
"category": "flow",
|
||||
"label": "Switch",
|
||||
"description": "Mehrere Zweige nach Wert",
|
||||
"label": t("Switch"),
|
||||
"description": t("Mehrere Zweige nach Wert"),
|
||||
"parameters": [
|
||||
{
|
||||
"name": "value",
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"frontendType": "text",
|
||||
"description": "Zu vergleichender Wert",
|
||||
"description": t("Zu vergleichender Wert"),
|
||||
},
|
||||
{
|
||||
"name": "cases",
|
||||
"type": "array",
|
||||
"required": False,
|
||||
"frontendType": "caseList",
|
||||
"description": "Fälle",
|
||||
"description": t("Fälle"),
|
||||
},
|
||||
],
|
||||
"inputs": 1,
|
||||
|
|
@ -55,15 +57,15 @@ FLOW_NODES = [
|
|||
{
|
||||
"id": "flow.loop",
|
||||
"category": "flow",
|
||||
"label": "Schleife / Für Jedes",
|
||||
"description": "Über Array-Elemente iterieren",
|
||||
"label": t("Schleife / Für Jedes"),
|
||||
"description": t("Über Array-Elemente iterieren"),
|
||||
"parameters": [
|
||||
{
|
||||
"name": "items",
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"frontendType": "text",
|
||||
"description": "Pfad zum Array",
|
||||
"description": t("Pfad zum Array"),
|
||||
},
|
||||
],
|
||||
"inputs": 1,
|
||||
|
|
@ -76,8 +78,8 @@ FLOW_NODES = [
|
|||
{
|
||||
"id": "flow.merge",
|
||||
"category": "flow",
|
||||
"label": "Zusammenführen",
|
||||
"description": "Mehrere Zweige zusammenführen",
|
||||
"label": t("Zusammenführen"),
|
||||
"description": t("Mehrere Zweige zusammenführen"),
|
||||
"parameters": [
|
||||
{
|
||||
"name": "mode",
|
||||
|
|
@ -85,7 +87,7 @@ FLOW_NODES = [
|
|||
"required": False,
|
||||
"frontendType": "select",
|
||||
"frontendOptions": {"options": ["first", "all", "append"]},
|
||||
"description": "Zusammenführungsmodus",
|
||||
"description": t("Zusammenführungsmodus"),
|
||||
"default": "first",
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Input/Human node definitions - nodes that require user action.
|
||||
|
||||
from modules.shared.i18nRegistry import t
|
||||
|
||||
INPUT_NODES = [
|
||||
{
|
||||
"id": "input.form",
|
||||
"category": "input",
|
||||
"label": "Formular",
|
||||
"description": "Benutzer füllt ein Formular aus",
|
||||
"label": t("Formular"),
|
||||
"description": t("Benutzer füllt ein Formular aus"),
|
||||
"parameters": [
|
||||
{
|
||||
"name": "fields",
|
||||
"type": "json",
|
||||
"required": True,
|
||||
"frontendType": "fieldBuilder",
|
||||
"description": "Formularfelder",
|
||||
"description": t("Formularfelder"),
|
||||
"default": [],
|
||||
},
|
||||
],
|
||||
|
|
@ -27,16 +29,16 @@ INPUT_NODES = [
|
|||
{
|
||||
"id": "input.approval",
|
||||
"category": "input",
|
||||
"label": "Genehmigung",
|
||||
"description": "Benutzer genehmigt oder lehnt ab",
|
||||
"label": t("Genehmigung"),
|
||||
"description": t("Benutzer genehmigt oder lehnt ab"),
|
||||
"parameters": [
|
||||
{"name": "title", "type": "string", "required": True, "frontendType": "text",
|
||||
"description": "Genehmigungstitel"},
|
||||
"description": t("Genehmigungstitel")},
|
||||
{"name": "description", "type": "string", "required": False, "frontendType": "textarea",
|
||||
"description": "Was genehmigt werden soll"},
|
||||
"description": t("Was genehmigt werden soll")},
|
||||
{"name": "approvalType", "type": "string", "required": False, "frontendType": "select",
|
||||
"frontendOptions": {"options": ["generic", "document"]},
|
||||
"description": "Typ: document oder generic", "default": "generic"},
|
||||
"description": t("Typ: document oder generic"), "default": "generic"},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -48,18 +50,18 @@ INPUT_NODES = [
|
|||
{
|
||||
"id": "input.upload",
|
||||
"category": "input",
|
||||
"label": "Upload",
|
||||
"description": "Benutzer lädt Datei(en) hoch",
|
||||
"label": t("Upload"),
|
||||
"description": t("Benutzer lädt Datei(en) hoch"),
|
||||
"parameters": [
|
||||
{"name": "accept", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Accept-String", "default": ""},
|
||||
"description": t("Accept-String"), "default": ""},
|
||||
{"name": "allowedTypes", "type": "json", "required": False, "frontendType": "multiselect",
|
||||
"frontendOptions": {"options": ["pdf", "docx", "xlsx", "pptx", "txt", "csv", "jpg", "png", "gif"]},
|
||||
"description": "Ausgewählte Dateitypen", "default": []},
|
||||
"description": t("Ausgewählte Dateitypen"), "default": []},
|
||||
{"name": "maxSize", "type": "number", "required": False, "frontendType": "number",
|
||||
"description": "Max. Dateigröße in MB", "default": 10},
|
||||
"description": t("Max. Dateigröße in MB"), "default": 10},
|
||||
{"name": "multiple", "type": "boolean", "required": False, "frontendType": "checkbox",
|
||||
"description": "Mehrere Dateien erlauben", "default": False},
|
||||
"description": t("Mehrere Dateien erlauben"), "default": False},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -71,13 +73,13 @@ INPUT_NODES = [
|
|||
{
|
||||
"id": "input.comment",
|
||||
"category": "input",
|
||||
"label": "Kommentar",
|
||||
"description": "Benutzer fügt einen Kommentar hinzu",
|
||||
"label": t("Kommentar"),
|
||||
"description": t("Benutzer fügt einen Kommentar hinzu"),
|
||||
"parameters": [
|
||||
{"name": "placeholder", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Platzhalter", "default": ""},
|
||||
"description": t("Platzhalter"), "default": ""},
|
||||
{"name": "required", "type": "boolean", "required": False, "frontendType": "checkbox",
|
||||
"description": "Kommentar erforderlich", "default": True},
|
||||
"description": t("Kommentar erforderlich"), "default": True},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -89,14 +91,14 @@ INPUT_NODES = [
|
|||
{
|
||||
"id": "input.review",
|
||||
"category": "input",
|
||||
"label": "Prüfung",
|
||||
"description": "Benutzer prüft Inhalt",
|
||||
"label": t("Prüfung"),
|
||||
"description": t("Benutzer prüft Inhalt"),
|
||||
"parameters": [
|
||||
{"name": "contentRef", "type": "string", "required": True, "frontendType": "text",
|
||||
"description": "Referenz auf Inhalt"},
|
||||
"description": t("Referenz auf Inhalt")},
|
||||
{"name": "reviewType", "type": "string", "required": False, "frontendType": "select",
|
||||
"frontendOptions": {"options": ["generic", "document"]},
|
||||
"description": "Art der Prüfung", "default": "generic"},
|
||||
"description": t("Art der Prüfung"), "default": "generic"},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -108,13 +110,13 @@ INPUT_NODES = [
|
|||
{
|
||||
"id": "input.selection",
|
||||
"category": "input",
|
||||
"label": "Auswahl",
|
||||
"description": "Benutzer wählt aus Optionen",
|
||||
"label": t("Auswahl"),
|
||||
"description": t("Benutzer wählt aus Optionen"),
|
||||
"parameters": [
|
||||
{"name": "options", "type": "json", "required": True, "frontendType": "keyValueRows",
|
||||
"description": "Optionen", "default": []},
|
||||
"description": t("Optionen"), "default": []},
|
||||
{"name": "multiple", "type": "boolean", "required": False, "frontendType": "checkbox",
|
||||
"description": "Mehrfachauswahl erlauben", "default": False},
|
||||
"description": t("Mehrfachauswahl erlauben"), "default": False},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -126,15 +128,15 @@ INPUT_NODES = [
|
|||
{
|
||||
"id": "input.confirmation",
|
||||
"category": "input",
|
||||
"label": "Bestätigung",
|
||||
"description": "Benutzer bestätigt Ja/Nein",
|
||||
"label": t("Bestätigung"),
|
||||
"description": t("Benutzer bestätigt Ja/Nein"),
|
||||
"parameters": [
|
||||
{"name": "question", "type": "string", "required": True, "frontendType": "text",
|
||||
"description": "Zu bestätigende Frage"},
|
||||
"description": t("Zu bestätigende Frage")},
|
||||
{"name": "confirmLabel", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Label für Bestätigen-Button", "default": "Confirm"},
|
||||
"description": t("Label für Bestätigen-Button"), "default": "Confirm"},
|
||||
{"name": "rejectLabel", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Label für Ablehnen-Button", "default": "Reject"},
|
||||
"description": t("Label für Ablehnen-Button"), "default": "Reject"},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
|
|||
|
|
@ -1,21 +1,23 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# SharePoint node definitions - map to methodSharepoint actions.
|
||||
|
||||
from modules.shared.i18nRegistry import t
|
||||
|
||||
SHAREPOINT_NODES = [
|
||||
{
|
||||
"id": "sharepoint.findFile",
|
||||
"category": "sharepoint",
|
||||
"label": "Datei finden",
|
||||
"description": "Datei nach Pfad oder Suche finden",
|
||||
"label": t("Datei finden"),
|
||||
"description": t("Datei nach Pfad oder Suche finden"),
|
||||
"parameters": [
|
||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||
"description": "SharePoint-Verbindung"},
|
||||
"description": t("SharePoint-Verbindung")},
|
||||
{"name": "searchQuery", "type": "string", "required": True, "frontendType": "text",
|
||||
"description": "Suchanfrage oder Pfad"},
|
||||
"description": t("Suchanfrage oder Pfad")},
|
||||
{"name": "site", "type": "string", "required": False, "frontendType": "text",
|
||||
"description": "Optionaler Site-Hinweis", "default": ""},
|
||||
"description": t("Optionaler Site-Hinweis"), "default": ""},
|
||||
{"name": "maxResults", "type": "number", "required": False, "frontendType": "number",
|
||||
"description": "Max Ergebnisse", "default": 1000},
|
||||
"description": t("Max Ergebnisse"), "default": 1000},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -28,14 +30,14 @@ SHAREPOINT_NODES = [
|
|||
{
|
||||
"id": "sharepoint.readFile",
|
||||
"category": "sharepoint",
|
||||
"label": "Datei lesen",
|
||||
"description": "Inhalt aus Datei extrahieren",
|
||||
"label": t("Datei lesen"),
|
||||
"description": t("Inhalt aus Datei extrahieren"),
|
||||
"parameters": [
|
||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||
"description": "SharePoint-Verbindung"},
|
||||
"description": t("SharePoint-Verbindung")},
|
||||
{"name": "pathQuery", "type": "string", "required": True, "frontendType": "sharepointFile",
|
||||
"frontendOptions": {"dependsOn": "connectionReference"},
|
||||
"description": "Dateipfad"},
|
||||
"description": t("Dateipfad")},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -48,14 +50,14 @@ SHAREPOINT_NODES = [
|
|||
{
|
||||
"id": "sharepoint.uploadFile",
|
||||
"category": "sharepoint",
|
||||
"label": "Datei hochladen",
|
||||
"description": "Datei zu SharePoint hochladen",
|
||||
"label": t("Datei hochladen"),
|
||||
"description": t("Datei zu SharePoint hochladen"),
|
||||
"parameters": [
|
||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||
"description": "SharePoint-Verbindung"},
|
||||
"description": t("SharePoint-Verbindung")},
|
||||
{"name": "pathQuery", "type": "string", "required": True, "frontendType": "sharepointFolder",
|
||||
"frontendOptions": {"dependsOn": "connectionReference"},
|
||||
"description": "Zielordner-Pfad"},
|
||||
"description": t("Zielordner-Pfad")},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -68,14 +70,14 @@ SHAREPOINT_NODES = [
|
|||
{
|
||||
"id": "sharepoint.listFiles",
|
||||
"category": "sharepoint",
|
||||
"label": "Dateien auflisten",
|
||||
"description": "Dateien in Ordner auflisten",
|
||||
"label": t("Dateien auflisten"),
|
||||
"description": t("Dateien in Ordner auflisten"),
|
||||
"parameters": [
|
||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||
"description": "SharePoint-Verbindung"},
|
||||
"description": t("SharePoint-Verbindung")},
|
||||
{"name": "pathQuery", "type": "string", "required": False, "frontendType": "sharepointFolder",
|
||||
"frontendOptions": {"dependsOn": "connectionReference"},
|
||||
"description": "Ordnerpfad", "default": "/"},
|
||||
"description": t("Ordnerpfad"), "default": "/"},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -88,14 +90,14 @@ SHAREPOINT_NODES = [
|
|||
{
|
||||
"id": "sharepoint.downloadFile",
|
||||
"category": "sharepoint",
|
||||
"label": "Datei herunterladen",
|
||||
"description": "Datei vom Pfad herunterladen",
|
||||
"label": t("Datei herunterladen"),
|
||||
"description": t("Datei vom Pfad herunterladen"),
|
||||
"parameters": [
|
||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||
"description": "SharePoint-Verbindung"},
|
||||
"description": t("SharePoint-Verbindung")},
|
||||
{"name": "pathQuery", "type": "string", "required": True, "frontendType": "sharepointFile",
|
||||
"frontendOptions": {"dependsOn": "connectionReference"},
|
||||
"description": "Vollständiger Dateipfad"},
|
||||
"description": t("Vollständiger Dateipfad")},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -108,17 +110,17 @@ SHAREPOINT_NODES = [
|
|||
{
|
||||
"id": "sharepoint.copyFile",
|
||||
"category": "sharepoint",
|
||||
"label": "Datei kopieren",
|
||||
"description": "Datei an Ziel kopieren",
|
||||
"label": t("Datei kopieren"),
|
||||
"description": t("Datei an Ziel kopieren"),
|
||||
"parameters": [
|
||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||
"description": "SharePoint-Verbindung"},
|
||||
"description": t("SharePoint-Verbindung")},
|
||||
{"name": "sourcePath", "type": "string", "required": True, "frontendType": "sharepointFile",
|
||||
"frontendOptions": {"dependsOn": "connectionReference"},
|
||||
"description": "Quelldatei-Pfad"},
|
||||
"description": t("Quelldatei-Pfad")},
|
||||
{"name": "destPath", "type": "string", "required": True, "frontendType": "sharepointFolder",
|
||||
"frontendOptions": {"dependsOn": "connectionReference"},
|
||||
"description": "Zielordner"},
|
||||
"description": t("Zielordner")},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Canvas start nodes — variant reflects workflow configuration (gear in editor).
|
||||
|
||||
from modules.shared.i18nRegistry import t
|
||||
|
||||
TRIGGER_NODES = [
|
||||
{
|
||||
"id": "trigger.manual",
|
||||
"category": "trigger",
|
||||
"label": "Start",
|
||||
"description": "Manuell, API oder Hintergrund-Starts (Webhook, E-Mail, …).",
|
||||
"label": t("Start"),
|
||||
"description": t("Manuell, API oder Hintergrund-Starts (Webhook, E-Mail, …)."),
|
||||
"parameters": [],
|
||||
"inputs": 0,
|
||||
"outputs": 1,
|
||||
|
|
@ -18,15 +20,15 @@ TRIGGER_NODES = [
|
|||
{
|
||||
"id": "trigger.form",
|
||||
"category": "trigger",
|
||||
"label": "Start (Formular)",
|
||||
"description": "Felder werden beim Start befüllt; konfigurieren Sie die Felder auf dieser Node.",
|
||||
"label": t("Start (Formular)"),
|
||||
"description": t("Felder werden beim Start befüllt; konfigurieren Sie die Felder auf dieser Node."),
|
||||
"parameters": [
|
||||
{
|
||||
"name": "formFields",
|
||||
"type": "json",
|
||||
"required": False,
|
||||
"frontendType": "fieldBuilder",
|
||||
"description": "Felddefinitionen",
|
||||
"description": t("Felddefinitionen"),
|
||||
},
|
||||
],
|
||||
"inputs": 0,
|
||||
|
|
@ -39,15 +41,15 @@ TRIGGER_NODES = [
|
|||
{
|
||||
"id": "trigger.schedule",
|
||||
"category": "trigger",
|
||||
"label": "Start (Zeitplan)",
|
||||
"description": "Cron-Ausdruck für geplante Läufe.",
|
||||
"label": t("Start (Zeitplan)"),
|
||||
"description": t("Cron-Ausdruck für geplante Läufe."),
|
||||
"parameters": [
|
||||
{
|
||||
"name": "cron",
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"frontendType": "cron",
|
||||
"description": "Cron-Ausdruck",
|
||||
"description": t("Cron-Ausdruck"),
|
||||
},
|
||||
],
|
||||
"inputs": 0,
|
||||
|
|
|
|||
|
|
@ -1,21 +1,23 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Trustee node definitions - map to methodTrustee actions.
|
||||
|
||||
from modules.shared.i18nRegistry import t
|
||||
|
||||
TRUSTEE_NODES = [
|
||||
{
|
||||
"id": "trustee.refreshAccountingData",
|
||||
"category": "trustee",
|
||||
"label": "Buchhaltungsdaten aktualisieren",
|
||||
"description": "Buchhaltungsdaten aus externem System importieren/aktualisieren.",
|
||||
"label": t("Buchhaltungsdaten aktualisieren"),
|
||||
"description": t("Buchhaltungsdaten aus externem System importieren/aktualisieren."),
|
||||
"parameters": [
|
||||
{"name": "featureInstanceId", "type": "string", "required": True, "frontendType": "hidden",
|
||||
"description": "Trustee Feature-Instanz-ID"},
|
||||
"description": t("Trustee Feature-Instanz-ID")},
|
||||
{"name": "forceRefresh", "type": "boolean", "required": False, "frontendType": "checkbox",
|
||||
"description": "Import erzwingen", "default": False},
|
||||
"description": t("Import erzwingen"), "default": False},
|
||||
{"name": "dateFrom", "type": "string", "required": False, "frontendType": "date",
|
||||
"description": "Startdatum", "default": ""},
|
||||
"description": t("Startdatum"), "default": ""},
|
||||
{"name": "dateTo", "type": "string", "required": False, "frontendType": "date",
|
||||
"description": "Enddatum", "default": ""},
|
||||
"description": t("Enddatum"), "default": ""},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -28,18 +30,18 @@ TRUSTEE_NODES = [
|
|||
{
|
||||
"id": "trustee.extractFromFiles",
|
||||
"category": "trustee",
|
||||
"label": "Dokumente extrahieren",
|
||||
"description": "Dokumenttyp und Daten aus PDF/JPG per AI extrahieren.",
|
||||
"label": t("Dokumente extrahieren"),
|
||||
"description": t("Dokumenttyp und Daten aus PDF/JPG per AI extrahieren."),
|
||||
"parameters": [
|
||||
{"name": "connectionReference", "type": "string", "required": False, "frontendType": "userConnection",
|
||||
"description": "SharePoint-Verbindung", "default": ""},
|
||||
"description": t("SharePoint-Verbindung"), "default": ""},
|
||||
{"name": "sharepointFolder", "type": "string", "required": False, "frontendType": "sharepointFolder",
|
||||
"frontendOptions": {"dependsOn": "connectionReference"},
|
||||
"description": "SharePoint-Ordnerpfad", "default": ""},
|
||||
"description": t("SharePoint-Ordnerpfad"), "default": ""},
|
||||
{"name": "featureInstanceId", "type": "string", "required": True, "frontendType": "hidden",
|
||||
"description": "Trustee Feature-Instanz-ID"},
|
||||
"description": t("Trustee Feature-Instanz-ID")},
|
||||
{"name": "prompt", "type": "string", "required": False, "frontendType": "textarea",
|
||||
"description": "AI-Prompt für Extraktion", "default": ""},
|
||||
"description": t("AI-Prompt für Extraktion"), "default": ""},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -52,13 +54,13 @@ TRUSTEE_NODES = [
|
|||
{
|
||||
"id": "trustee.processDocuments",
|
||||
"category": "trustee",
|
||||
"label": "Dokumente verarbeiten",
|
||||
"description": "TrusteeDocument + TrusteePosition aus Extraktionsergebnis erstellen.",
|
||||
"label": t("Dokumente verarbeiten"),
|
||||
"description": t("TrusteeDocument + TrusteePosition aus Extraktionsergebnis erstellen."),
|
||||
"parameters": [
|
||||
{"name": "documentList", "type": "string", "required": False, "frontendType": "hidden",
|
||||
"description": "Automatisch via Wire-Verbindung befüllt"},
|
||||
"description": t("Automatisch via Wire-Verbindung befüllt")},
|
||||
{"name": "featureInstanceId", "type": "string", "required": True, "frontendType": "hidden",
|
||||
"description": "Trustee Feature-Instanz-ID"},
|
||||
"description": t("Trustee Feature-Instanz-ID")},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
@ -71,13 +73,13 @@ TRUSTEE_NODES = [
|
|||
{
|
||||
"id": "trustee.syncToAccounting",
|
||||
"category": "trustee",
|
||||
"label": "In Buchhaltung synchronisieren",
|
||||
"description": "Trustee-Positionen in Buchhaltungssystem übertragen.",
|
||||
"label": t("In Buchhaltung synchronisieren"),
|
||||
"description": t("Trustee-Positionen in Buchhaltungssystem übertragen."),
|
||||
"parameters": [
|
||||
{"name": "documentList", "type": "string", "required": False, "frontendType": "hidden",
|
||||
"description": "Automatisch via Wire-Verbindung befüllt"},
|
||||
"description": t("Automatisch via Wire-Verbindung befüllt")},
|
||||
{"name": "featureInstanceId", "type": "string", "required": True, "frontendType": "hidden",
|
||||
"description": "Trustee Feature-Instanz-ID"},
|
||||
"description": t("Trustee Feature-Instanz-ID")},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from typing import Dict, List, Any
|
|||
|
||||
from modules.features.graphicalEditor.nodeDefinitions import STATIC_NODE_TYPES
|
||||
from modules.features.graphicalEditor.portTypes import PORT_TYPE_CATALOG, SYSTEM_VARIABLES
|
||||
from modules.shared.i18nRegistry import normalizePrimaryLanguageTag
|
||||
from modules.shared.i18nRegistry import normalizePrimaryLanguageTag, resolveText
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -41,27 +41,34 @@ def _pickFromLangMap(d: Any, lang: str) -> Any:
|
|||
|
||||
|
||||
def _localizeNode(node: Dict[str, Any], language: str) -> Dict[str, Any]:
|
||||
"""Apply language to label/description/parameters. Keep inputPorts/outputPorts."""
|
||||
"""Apply request language via resolveText (t() keys + multilingual dicts)."""
|
||||
lang = normalizePrimaryLanguageTag(language, "en")
|
||||
out = dict(node)
|
||||
for key in list(out.keys()):
|
||||
if key.startswith("_"):
|
||||
del out[key]
|
||||
if isinstance(node.get("label"), dict):
|
||||
out["label"] = _pickFromLangMap(node["label"], lang) or node.get("id", "")
|
||||
if isinstance(node.get("description"), dict):
|
||||
out["description"] = _pickFromLangMap(node["description"], lang) or ""
|
||||
lbl = node.get("label")
|
||||
if lbl is not None:
|
||||
out["label"] = resolveText(lbl, lang) or node.get("id", "")
|
||||
desc = node.get("description")
|
||||
if desc is not None:
|
||||
out["description"] = resolveText(desc, lang)
|
||||
ol = node.get("outputLabels")
|
||||
if isinstance(ol, dict) and ol:
|
||||
if ol is not None:
|
||||
if isinstance(ol, list):
|
||||
out["outputLabels"] = [resolveText(x, lang) for x in ol]
|
||||
elif isinstance(ol, dict) and ol:
|
||||
first = next(iter(ol.values()), None)
|
||||
if isinstance(first, (list, tuple)):
|
||||
picked = _pickFromLangMap(ol, lang)
|
||||
out["outputLabels"] = picked if picked is not None else list(first)
|
||||
raw = list(picked) if picked is not None else list(first)
|
||||
out["outputLabels"] = [resolveText(x, lang) for x in raw]
|
||||
params = []
|
||||
for p in node.get("parameters", []):
|
||||
pc = dict(p)
|
||||
if isinstance(p.get("description"), dict):
|
||||
pc["description"] = _pickFromLangMap(p["description"], lang) or str(p.get("description", ""))
|
||||
pd = p.get("description")
|
||||
if pd is not None:
|
||||
pc["description"] = resolveText(pd, lang)
|
||||
params.append(pc)
|
||||
out["parameters"] = params
|
||||
return out
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import os
|
|||
from typing import Dict, List, Optional
|
||||
|
||||
from .accountingConnectorBase import BaseAccountingConnector
|
||||
from modules.shared.i18nRegistry import resolveText
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -62,11 +61,11 @@ class AccountingRegistry:
|
|||
fields = []
|
||||
for f in connector.getRequiredConfigFields():
|
||||
fd = f.model_dump()
|
||||
fd["label"] = resolveText(f.label)
|
||||
fd["label"] = f.label
|
||||
fields.append(fd)
|
||||
result.append({
|
||||
"connectorType": connectorType,
|
||||
"label": resolveText(connector.getConnectorLabel()),
|
||||
"label": connector.getConnectorLabel(),
|
||||
"configFields": fields,
|
||||
})
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ from ..accountingConnectorBase import (
|
|||
ConnectorConfigField,
|
||||
SyncResult,
|
||||
)
|
||||
from modules.shared.i18nRegistry import t
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -41,27 +42,27 @@ class AccountingConnectorAbacus(BaseAccountingConnector):
|
|||
return [
|
||||
ConnectorConfigField(
|
||||
key="apiBaseUrl",
|
||||
label="API Base URL",
|
||||
label=t("API Base URL"),
|
||||
fieldType="text",
|
||||
secret=False,
|
||||
placeholder="e.g. https://abacus.meinefirma.ch/api/entity/v1/",
|
||||
),
|
||||
ConnectorConfigField(
|
||||
key="clientName",
|
||||
label="Mandantenname",
|
||||
label=t("Mandantenname"),
|
||||
fieldType="text",
|
||||
secret=False,
|
||||
placeholder="e.g. 7777",
|
||||
),
|
||||
ConnectorConfigField(
|
||||
key="clientId",
|
||||
label="Client-ID",
|
||||
label=t("Client-ID"),
|
||||
fieldType="text",
|
||||
secret=False,
|
||||
),
|
||||
ConnectorConfigField(
|
||||
key="clientSecret",
|
||||
label="Client-Secret",
|
||||
label=t("Client-Secret"),
|
||||
fieldType="password",
|
||||
secret=True,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ from ..accountingConnectorBase import (
|
|||
ConnectorConfigField,
|
||||
SyncResult,
|
||||
)
|
||||
from modules.shared.i18nRegistry import t
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -42,21 +43,21 @@ class AccountingConnectorBexio(BaseAccountingConnector):
|
|||
return [
|
||||
ConnectorConfigField(
|
||||
key="apiBaseUrl",
|
||||
label="API Base URL",
|
||||
label=t("API Base URL"),
|
||||
fieldType="text",
|
||||
secret=False,
|
||||
placeholder="https://api.bexio.com/",
|
||||
),
|
||||
ConnectorConfigField(
|
||||
key="clientName",
|
||||
label="Mandantenname",
|
||||
label=t("Mandantenname"),
|
||||
fieldType="text",
|
||||
secret=False,
|
||||
placeholder="e.g. poweronag",
|
||||
),
|
||||
ConnectorConfigField(
|
||||
key="accessToken",
|
||||
label="Persönlicher Zugriffstoken",
|
||||
label=t("Persönlicher Zugriffstoken"),
|
||||
fieldType="password",
|
||||
secret=True,
|
||||
placeholder="PAT from developer.bexio.com",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ from ..accountingConnectorBase import (
|
|||
ConnectorConfigField,
|
||||
SyncResult,
|
||||
)
|
||||
from modules.shared.i18nRegistry import t
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -42,21 +43,21 @@ class AccountingConnectorRma(BaseAccountingConnector):
|
|||
return [
|
||||
ConnectorConfigField(
|
||||
key="apiBaseUrl",
|
||||
label="API Base URL",
|
||||
label=t("API Base URL"),
|
||||
fieldType="text",
|
||||
secret=False,
|
||||
placeholder="https://service.runmyaccounts.com/api/latest/clients/",
|
||||
),
|
||||
ConnectorConfigField(
|
||||
key="clientName",
|
||||
label="Mandantenname",
|
||||
label=t("Mandantenname"),
|
||||
fieldType="text",
|
||||
secret=False,
|
||||
placeholder="e.g. meinefirma",
|
||||
),
|
||||
ConnectorConfigField(
|
||||
key="apiKey",
|
||||
label="API-Schlüssel",
|
||||
label=t("API-Schlüssel"),
|
||||
fieldType="password",
|
||||
secret=True,
|
||||
),
|
||||
|
|
@ -227,6 +228,10 @@ class AccountingConnectorRma(BaseAccountingConnector):
|
|||
if rawDesc and len(rawDesc) > 80:
|
||||
payload["notes"] = rawDesc[:2000]
|
||||
|
||||
logger.debug("RMA pushBooking payload: batch=%s transdate=%s accounts=%s",
|
||||
batchNumber, transdate,
|
||||
[(t.get("accno"), t.get("debit_amount"), t.get("credit_amount")) for t in glTransactions])
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
url = self._buildUrl(config, "gl")
|
||||
async with session.post(url, headers=self._buildHeaders(config), json=payload, timeout=aiohttp.ClientTimeout(total=30)) as resp:
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ from modules.auth.authentication import getRequestContext, RequestContext
|
|||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
from modules.connectors.connectorDbPostgre import DatabaseConnector
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
from modules.datamodels.datamodelPagination import PaginationParams
|
||||
from modules.datamodels.datamodelPagination import PaginationParams, normalize_pagination_dict
|
||||
from modules.features.graphicalEditor.datamodelFeatureGraphicalEditor import (
|
||||
AutoRun, AutoStepLog, AutoWorkflow, AutoTask,
|
||||
)
|
||||
|
|
@ -152,6 +152,7 @@ def get_workflow_runs(
|
|||
offset: int = Query(0, ge=0),
|
||||
status: Optional[str] = Query(None, description="Filter by status"),
|
||||
mandateId: Optional[str] = Query(None, description="Filter by mandate"),
|
||||
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams"),
|
||||
context: RequestContext = Depends(getRequestContext),
|
||||
) -> dict:
|
||||
"""List workflow runs with RBAC scoping (SQL-paginated)."""
|
||||
|
|
@ -167,8 +168,19 @@ def get_workflow_runs(
|
|||
if mandateId:
|
||||
recordFilter["mandateId"] = mandateId
|
||||
|
||||
paginationParams = None
|
||||
if pagination:
|
||||
try:
|
||||
paginationDict = json.loads(pagination)
|
||||
if paginationDict:
|
||||
paginationDict = normalize_pagination_dict(paginationDict)
|
||||
paginationParams = PaginationParams(**paginationDict)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not paginationParams:
|
||||
page = (offset // limit) + 1 if limit > 0 else 1
|
||||
pagination = PaginationParams(
|
||||
paginationParams = PaginationParams(
|
||||
page=page,
|
||||
pageSize=limit,
|
||||
sort=[{"field": "sysCreatedAt", "direction": "desc"}],
|
||||
|
|
@ -176,7 +188,7 @@ def get_workflow_runs(
|
|||
|
||||
result = db.getRecordsetPaginated(
|
||||
AutoRun,
|
||||
pagination=pagination,
|
||||
pagination=paginationParams,
|
||||
recordFilter=recordFilter if recordFilter else None,
|
||||
)
|
||||
pageRuns = result.get("items", []) if isinstance(result, dict) else result.items
|
||||
|
|
@ -317,44 +329,6 @@ def get_workflow_metrics(
|
|||
}
|
||||
|
||||
|
||||
@router.get("/{runId}/steps")
|
||||
@limiter.limit("60/minute")
|
||||
def get_run_steps(
|
||||
request: Request,
|
||||
runId: str = Path(..., description="Run ID"),
|
||||
context: RequestContext = Depends(getRequestContext),
|
||||
) -> dict:
|
||||
"""Get step logs for a specific run (with access check)."""
|
||||
db = _getDb()
|
||||
if not db._ensureTableExists(AutoRun):
|
||||
raise HTTPException(status_code=404, detail=routeApiMsg("Run not found"))
|
||||
|
||||
runs = db.getRecordset(AutoRun, recordFilter={"id": runId})
|
||||
if not runs:
|
||||
raise HTTPException(status_code=404, detail=routeApiMsg("Run not found"))
|
||||
run = dict(runs[0])
|
||||
|
||||
if not context.hasSysAdminRole:
|
||||
userId = str(context.user.id) if context.user else None
|
||||
runOwner = run.get("ownerId")
|
||||
runMandate = run.get("mandateId")
|
||||
|
||||
if runOwner == userId:
|
||||
pass
|
||||
elif runMandate and userId and _isUserMandateAdmin(userId, runMandate):
|
||||
pass
|
||||
else:
|
||||
raise HTTPException(status_code=403, detail=routeApiMsg("Access denied"))
|
||||
|
||||
if not db._ensureTableExists(AutoStepLog):
|
||||
return {"steps": []}
|
||||
|
||||
records = db.getRecordset(AutoStepLog, recordFilter={"runId": runId})
|
||||
steps = [dict(r) for r in records] if records else []
|
||||
steps.sort(key=lambda s: s.get("startedAt") or 0)
|
||||
return {"steps": steps}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# System-level Workflow listing (all workflows the user can see via RBAC)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -385,7 +359,10 @@ def get_system_workflows(
|
|||
paginationParams = None
|
||||
if pagination:
|
||||
try:
|
||||
paginationParams = PaginationParams(**json.loads(pagination))
|
||||
paginationDict = json.loads(pagination)
|
||||
if paginationDict:
|
||||
paginationDict = normalize_pagination_dict(paginationDict)
|
||||
paginationParams = PaginationParams(**paginationDict)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
@ -492,6 +469,161 @@ def get_system_workflows(
|
|||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Filter-values endpoints (for FormGeneratorTable column filters)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _enrichedFilterValues(
|
||||
db, context: RequestContext, modelClass, scopeFilter, column: str,
|
||||
) -> List[str]:
|
||||
"""Return distinct filter values for enriched columns (mandateLabel, instanceLabel)
|
||||
or delegate to DB-level DISTINCT for raw columns."""
|
||||
if column in ("mandateLabel", "mandateId"):
|
||||
baseFilter = scopeFilter(context)
|
||||
recordFilter = dict(baseFilter) if baseFilter else {}
|
||||
if modelClass == AutoWorkflow:
|
||||
recordFilter["isTemplate"] = False
|
||||
items = db.getRecordset(modelClass, recordFilter=recordFilter or None, fieldFilter=["mandateId"]) or []
|
||||
mandateIds = list({r.get("mandateId") for r in items if r.get("mandateId")})
|
||||
if not mandateIds:
|
||||
return []
|
||||
try:
|
||||
rootIface = getRootInterface()
|
||||
mMap = rootIface.getMandatesByIds(mandateIds)
|
||||
labels = sorted({
|
||||
getattr(m, "label", None) or getattr(m, "name", mid) or mid
|
||||
for mid, m in mMap.items()
|
||||
}, key=lambda v: v.lower())
|
||||
return labels
|
||||
except Exception:
|
||||
return sorted(mandateIds)
|
||||
|
||||
if column in ("instanceLabel", "featureInstanceId"):
|
||||
baseFilter = scopeFilter(context)
|
||||
recordFilter = dict(baseFilter) if baseFilter else {}
|
||||
if modelClass == AutoWorkflow:
|
||||
recordFilter["isTemplate"] = False
|
||||
items = db.getRecordset(modelClass, recordFilter=recordFilter or None, fieldFilter=["featureInstanceId"]) or []
|
||||
instanceIds = list({r.get("featureInstanceId") for r in items if r.get("featureInstanceId")})
|
||||
else:
|
||||
items = db.getRecordset(modelClass, recordFilter=recordFilter or None, fieldFilter=["workflowId"]) or []
|
||||
wfIds = list({r.get("workflowId") for r in items if r.get("workflowId")})
|
||||
instanceIds = []
|
||||
if wfIds and db._ensureTableExists(AutoWorkflow):
|
||||
wfs = db.getRecordset(AutoWorkflow, recordFilter={"id": wfIds}, fieldFilter=["featureInstanceId"]) or []
|
||||
instanceIds = list({w.get("featureInstanceId") for w in wfs if w.get("featureInstanceId")})
|
||||
if not instanceIds:
|
||||
return []
|
||||
try:
|
||||
from modules.interfaces.interfaceFeatures import getFeatureInterface
|
||||
rootIface = getRootInterface()
|
||||
featureIface = getFeatureInterface(rootIface.db)
|
||||
labels = []
|
||||
for iid in instanceIds:
|
||||
fi = featureIface.getFeatureInstance(iid)
|
||||
if fi:
|
||||
labels.append(fi.label or iid)
|
||||
return sorted(set(labels), key=lambda v: v.lower())
|
||||
except Exception:
|
||||
return sorted(instanceIds)
|
||||
|
||||
if column == "workflowLabel":
|
||||
baseFilter = scopeFilter(context)
|
||||
recordFilter = dict(baseFilter) if baseFilter else {}
|
||||
items = db.getRecordset(modelClass, recordFilter=recordFilter or None, fieldFilter=["workflowId", "label"]) or []
|
||||
labels = set()
|
||||
wfIds = set()
|
||||
for r in items:
|
||||
if r.get("label"):
|
||||
labels.add(r["label"])
|
||||
if r.get("workflowId"):
|
||||
wfIds.add(r["workflowId"])
|
||||
if wfIds and db._ensureTableExists(AutoWorkflow):
|
||||
wfs = db.getRecordset(AutoWorkflow, recordFilter={"id": list(wfIds)}, fieldFilter=["label"]) or []
|
||||
for wf in wfs:
|
||||
if wf.get("label"):
|
||||
labels.add(wf["label"])
|
||||
return sorted(labels, key=lambda v: v.lower())
|
||||
|
||||
baseFilter = scopeFilter(context)
|
||||
recordFilter = dict(baseFilter) if baseFilter else {}
|
||||
if modelClass == AutoWorkflow:
|
||||
recordFilter["isTemplate"] = False
|
||||
return db.getDistinctColumnValues(modelClass, column, recordFilter=recordFilter or None) or []
|
||||
|
||||
|
||||
@router.get("/filter-values")
|
||||
@limiter.limit("60/minute")
|
||||
def get_run_filter_values(
|
||||
request: Request,
|
||||
column: str = Query(..., description="Column key"),
|
||||
pagination: Optional[str] = Query(None, description="JSON-encoded current filters"),
|
||||
context: RequestContext = Depends(getRequestContext),
|
||||
) -> list:
|
||||
"""Return distinct filter values for a column in workflow runs."""
|
||||
db = _getDb()
|
||||
if not db._ensureTableExists(AutoRun):
|
||||
return []
|
||||
return _enrichedFilterValues(db, context, AutoRun, _scopedRunFilter, column)
|
||||
|
||||
|
||||
@router.get("/workflows/filter-values")
|
||||
@limiter.limit("60/minute")
|
||||
def get_workflow_filter_values(
|
||||
request: Request,
|
||||
column: str = Query(..., description="Column key"),
|
||||
pagination: Optional[str] = Query(None, description="JSON-encoded current filters"),
|
||||
context: RequestContext = Depends(getRequestContext),
|
||||
) -> list:
|
||||
"""Return distinct filter values for a column in workflows."""
|
||||
db = _getDb()
|
||||
if not db._ensureTableExists(AutoWorkflow):
|
||||
return []
|
||||
return _enrichedFilterValues(db, context, AutoWorkflow, _scopedWorkflowFilter, column)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Run-specific endpoints (path-param routes MUST come after static routes)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.get("/{runId}/steps")
|
||||
@limiter.limit("60/minute")
|
||||
def get_run_steps(
|
||||
request: Request,
|
||||
runId: str = Path(..., description="Run ID"),
|
||||
context: RequestContext = Depends(getRequestContext),
|
||||
) -> dict:
|
||||
"""Get step logs for a specific run (with access check)."""
|
||||
db = _getDb()
|
||||
if not db._ensureTableExists(AutoRun):
|
||||
raise HTTPException(status_code=404, detail=routeApiMsg("Run not found"))
|
||||
|
||||
runs = db.getRecordset(AutoRun, recordFilter={"id": runId})
|
||||
if not runs:
|
||||
raise HTTPException(status_code=404, detail=routeApiMsg("Run not found"))
|
||||
run = dict(runs[0])
|
||||
|
||||
if not context.hasSysAdminRole:
|
||||
userId = str(context.user.id) if context.user else None
|
||||
runOwner = run.get("ownerId")
|
||||
runMandate = run.get("mandateId")
|
||||
|
||||
if runOwner == userId:
|
||||
pass
|
||||
elif runMandate and userId and _isUserMandateAdmin(userId, runMandate):
|
||||
pass
|
||||
else:
|
||||
raise HTTPException(status_code=403, detail=routeApiMsg("Access denied"))
|
||||
|
||||
if not db._ensureTableExists(AutoStepLog):
|
||||
return {"steps": []}
|
||||
|
||||
records = db.getRecordset(AutoStepLog, recordFilter={"runId": runId})
|
||||
steps = [dict(r) for r in records] if records else []
|
||||
steps.sort(key=lambda s: s.get("startedAt") or 0)
|
||||
return {"steps": steps}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SSE stream for live run tracing (system-level, no instanceId required)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -294,7 +294,9 @@ class ActionNodeExecutor:
|
|||
resolvedParams["context"] = ctx
|
||||
|
||||
# 10. Pass upstream documents as documentList if available
|
||||
if "documentList" not in resolvedParams and 0 in inputSources:
|
||||
# Use truthiness check: empty values ([], "", None) from static graph params
|
||||
# must not block automatic upstream population via wire connections.
|
||||
if not resolvedParams.get("documentList") and 0 in inputSources:
|
||||
srcId, _ = inputSources[0]
|
||||
upstream = context.get("nodeOutputs", {}).get(srcId)
|
||||
if upstream and isinstance(upstream, dict):
|
||||
|
|
|
|||
35
tests/demo/README.md
Normal file
35
tests/demo/README.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Demo Test Suite
|
||||
|
||||
Automated tests for the investor demo configuration.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Gateway DB must be running and accessible
|
||||
2. Demo config must be loaded first: Admin UI → `/admin/demo-config` → Load "Investor Demo April 2026"
|
||||
3. RMA credentials must be set in `gateway/config.ini`
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
cd gateway/
|
||||
|
||||
# All demo tests (structural, no AI calls):
|
||||
pytest tests/demo/ -v
|
||||
|
||||
# Only bootstrap tests:
|
||||
pytest tests/demo/test_demo_bootstrap.py -v
|
||||
|
||||
# Only UC1 trustee:
|
||||
pytest tests/demo/test_demo_uc1_trustee.py -v
|
||||
```
|
||||
|
||||
## Test files
|
||||
|
||||
| File | What it tests |
|
||||
|------|--------------|
|
||||
| `test_demo_bootstrap.py` | Idempotent load/remove, mandates, user, features, RMA, neutralization |
|
||||
| `test_demo_uc1_trustee.py` | Trustee instances, RMA config, system workflow templates |
|
||||
| `test_demo_uc2_realestate.py` | Workspace instances for agent demo |
|
||||
| `test_demo_uc3_chatbot.py` | Chatbot instance, knowledge-base files |
|
||||
| `test_demo_uc4_i18n.py` | i18n readiness, Spanish not pre-installed |
|
||||
| `test_demo_neutralization.py` | Neutralization config enabled, test PDF exists |
|
||||
0
tests/demo/__init__.py
Normal file
0
tests/demo/__init__.py
Normal file
64
tests/demo/conftest.py
Normal file
64
tests/demo/conftest.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
"""
|
||||
Demo test fixtures.
|
||||
|
||||
Provides a live DB connector and helpers for the demo test suite.
|
||||
All tests assume the gateway is configured and the DB is reachable.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from modules.security.rootAccess import getRootDbAppConnector
|
||||
from modules.datamodels.datamodelUam import Mandate, UserInDB
|
||||
from modules.datamodels.datamodelFeatures import FeatureInstance
|
||||
from modules.datamodels.datamodelMembership import UserMandate
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def db():
|
||||
"""Root DB connector (session-scoped, reused across all tests)."""
|
||||
return getRootDbAppConnector()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def demoConfig():
|
||||
"""The investor demo config instance."""
|
||||
from modules.demoConfigs import _getDemoConfigByCode
|
||||
cfg = _getDemoConfigByCode("investor-demo-2026")
|
||||
assert cfg is not None, "Demo config 'investor-demo-2026' not found — check modules/demoConfigs/"
|
||||
return cfg
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Mandate helpers — function-scoped so they always reflect current DB state
|
||||
# (test_removeAndReload recreates mandates with new IDs mid-session)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture
|
||||
def mandateHappylife(db):
|
||||
"""HappyLife AG mandate (must exist after bootstrap load)."""
|
||||
records = db.getRecordset(Mandate, recordFilter={"name": "happylife"})
|
||||
assert records, "Mandate 'happylife' not found — run demo config load first"
|
||||
return records[0]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mandateAlpina(db):
|
||||
"""Alpina Treuhand AG mandate (must exist after bootstrap load)."""
|
||||
records = db.getRecordset(Mandate, recordFilter={"name": "alpina-treuhand"})
|
||||
assert records, "Mandate 'alpina-treuhand' not found — run demo config load first"
|
||||
return records[0]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def demoUser(db):
|
||||
"""Patrick Helvetia user (must exist after bootstrap load)."""
|
||||
records = db.getRecordset(UserInDB, recordFilter={"username": "patrick.helvetia"})
|
||||
assert records, "User 'patrick.helvetia' not found — run demo config load first"
|
||||
return records[0]
|
||||
|
||||
|
||||
def _getFeatureInstances(db, mandateId: str, featureCode: str):
|
||||
"""Helper: get feature instances for a mandate + code."""
|
||||
return db.getRecordset(FeatureInstance, recordFilter={
|
||||
"mandateId": mandateId,
|
||||
"featureCode": featureCode,
|
||||
})
|
||||
66
tests/demo/test_demo_api.py
Normal file
66
tests/demo/test_demo_api.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
"""
|
||||
T-API: Demo Config API endpoint verification.
|
||||
|
||||
Tests the admin API endpoints for listing, loading, and removing demo configs.
|
||||
Uses FastAPI TestClient (no running server needed).
|
||||
|
||||
Note: Login requires CSRF + form-data + httpOnly cookies, so we test
|
||||
unauthenticated rejection and the discovery module directly.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestDemoConfigDiscovery:
|
||||
"""Test the auto-discovery module (no HTTP needed)."""
|
||||
|
||||
def test_discoveryFindsInvestorConfig(self):
|
||||
from modules.demoConfigs import _getAvailableDemoConfigs
|
||||
configs = _getAvailableDemoConfigs()
|
||||
assert "investor-demo-2026" in configs, f"Available configs: {list(configs.keys())}"
|
||||
|
||||
def test_getByCodeReturnsInstance(self):
|
||||
from modules.demoConfigs import _getDemoConfigByCode
|
||||
cfg = _getDemoConfigByCode("investor-demo-2026")
|
||||
assert cfg is not None
|
||||
assert cfg.code == "investor-demo-2026"
|
||||
assert cfg.label == "Investor Demo April 2026"
|
||||
|
||||
def test_getByCodeReturnsNoneForUnknown(self):
|
||||
from modules.demoConfigs import _getDemoConfigByCode
|
||||
cfg = _getDemoConfigByCode("nonexistent-config")
|
||||
assert cfg is None
|
||||
|
||||
def test_toDictHasRequiredFields(self):
|
||||
from modules.demoConfigs import _getDemoConfigByCode
|
||||
cfg = _getDemoConfigByCode("investor-demo-2026")
|
||||
d = cfg.toDict()
|
||||
assert "code" in d
|
||||
assert "label" in d
|
||||
assert "description" in d
|
||||
assert d["code"] == "investor-demo-2026"
|
||||
|
||||
|
||||
class TestDemoConfigApiEndpoints:
|
||||
"""Test API endpoints via TestClient."""
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def client(self):
|
||||
try:
|
||||
from app import app
|
||||
from fastapi.testclient import TestClient
|
||||
return TestClient(app)
|
||||
except Exception as e:
|
||||
pytest.skip(f"Cannot create TestClient: {e}")
|
||||
|
||||
def test_listEndpointRejectsUnauthenticated(self, client):
|
||||
response = client.get("/api/admin/demo-config")
|
||||
assert response.status_code in (401, 403)
|
||||
|
||||
def test_loadEndpointRejectsUnauthenticated(self, client):
|
||||
response = client.post("/api/admin/demo-config/investor-demo-2026/load")
|
||||
assert response.status_code in (401, 403)
|
||||
|
||||
def test_removeEndpointRejectsUnauthenticated(self, client):
|
||||
response = client.post("/api/admin/demo-config/investor-demo-2026/remove")
|
||||
assert response.status_code in (401, 403)
|
||||
133
tests/demo/test_demo_bootstrap.py
Normal file
133
tests/demo/test_demo_bootstrap.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
"""
|
||||
T-BOOT: Bootstrap idempotency and demo state verification.
|
||||
|
||||
Tests that the demo config can be loaded twice without errors
|
||||
and that all expected objects exist afterwards.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from modules.datamodels.datamodelUam import Mandate, UserInDB
|
||||
from modules.datamodels.datamodelFeatures import FeatureInstance
|
||||
from modules.datamodels.datamodelMembership import UserMandate
|
||||
from tests.demo.conftest import _getFeatureInstances
|
||||
|
||||
|
||||
class TestDemoBootstrap:
|
||||
|
||||
def test_loadIsIdempotent(self, db, demoConfig):
|
||||
"""Loading the demo config twice must not raise errors."""
|
||||
summary1 = demoConfig.load(db)
|
||||
assert "errors" not in summary1 or len(summary1.get("errors", [])) == 0, f"First load errors: {summary1['errors']}"
|
||||
|
||||
summary2 = demoConfig.load(db)
|
||||
assert "errors" not in summary2 or len(summary2.get("errors", [])) == 0, f"Second load errors: {summary2['errors']}"
|
||||
|
||||
def test_mandateHappylifeExists(self, db):
|
||||
records = db.getRecordset(Mandate, recordFilter={"name": "happylife"})
|
||||
assert len(records) == 1
|
||||
assert records[0].get("label") == "HappyLife AG"
|
||||
assert records[0].get("enabled") is True
|
||||
|
||||
def test_mandateAlpinaExists(self, db):
|
||||
records = db.getRecordset(Mandate, recordFilter={"name": "alpina-treuhand"})
|
||||
assert len(records) == 1
|
||||
assert records[0].get("label") == "Alpina Treuhand AG"
|
||||
|
||||
def test_userPatrickExists(self, db):
|
||||
records = db.getRecordset(UserInDB, recordFilter={"username": "patrick.helvetia"})
|
||||
assert len(records) == 1
|
||||
user = records[0]
|
||||
assert user.get("email") == "p.motsch@poweron.swiss"
|
||||
assert user.get("isSysAdmin") is True
|
||||
assert user.get("language") == "en"
|
||||
|
||||
def test_userMembershipBothMandates(self, db, demoUser, mandateHappylife, mandateAlpina):
|
||||
userId = demoUser.get("id")
|
||||
for mandate in [mandateHappylife, mandateAlpina]:
|
||||
mid = mandate.get("id")
|
||||
memberships = db.getRecordset(UserMandate, recordFilter={"userId": userId, "mandateId": mid})
|
||||
assert len(memberships) >= 1, f"User not member of mandate {mandate.get('label')}"
|
||||
|
||||
@pytest.mark.parametrize("featureCode", ["workspace", "trustee", "graphicalEditor", "chatbot", "neutralization"])
|
||||
def test_happylifeFeaturesExist(self, db, mandateHappylife, featureCode):
|
||||
mid = mandateHappylife.get("id")
|
||||
instances = _getFeatureInstances(db, mid, featureCode)
|
||||
assert len(instances) >= 1, f"Feature '{featureCode}' missing in HappyLife AG"
|
||||
|
||||
@pytest.mark.parametrize("featureCode", ["workspace", "trustee", "graphicalEditor", "neutralization"])
|
||||
def test_alpinaFeaturesExist(self, db, mandateAlpina, featureCode):
|
||||
mid = mandateAlpina.get("id")
|
||||
instances = _getFeatureInstances(db, mid, featureCode)
|
||||
assert len(instances) >= 1, f"Feature '{featureCode}' missing in Alpina Treuhand AG"
|
||||
|
||||
def test_alpinaNoChatbot(self, db, mandateAlpina):
|
||||
"""Alpina should NOT have a chatbot instance."""
|
||||
mid = mandateAlpina.get("id")
|
||||
instances = _getFeatureInstances(db, mid, "chatbot")
|
||||
assert len(instances) == 0, "Alpina Treuhand should not have chatbot"
|
||||
|
||||
|
||||
class TestDemoBootstrapRma:
|
||||
|
||||
def test_trusteeRmaConfigHappylife(self, db, mandateHappylife):
|
||||
from modules.features.trustee.datamodelFeatureTrustee import TrusteeAccountingConfig
|
||||
mid = mandateHappylife.get("id")
|
||||
instances = _getFeatureInstances(db, mid, "trustee")
|
||||
assert instances, "No trustee instance in HappyLife"
|
||||
iid = instances[0].get("id")
|
||||
configs = db.getRecordset(TrusteeAccountingConfig, recordFilter={"featureInstanceId": iid})
|
||||
assert len(configs) >= 1, "No RMA config for HappyLife trustee"
|
||||
assert configs[0].get("connectorType") == "rma"
|
||||
assert configs[0].get("isActive") is True
|
||||
|
||||
def test_trusteeRmaConfigAlpina(self, db, mandateAlpina):
|
||||
from modules.features.trustee.datamodelFeatureTrustee import TrusteeAccountingConfig
|
||||
mid = mandateAlpina.get("id")
|
||||
instances = _getFeatureInstances(db, mid, "trustee")
|
||||
assert instances, "No trustee instance in Alpina"
|
||||
iid = instances[0].get("id")
|
||||
configs = db.getRecordset(TrusteeAccountingConfig, recordFilter={"featureInstanceId": iid})
|
||||
assert len(configs) >= 1, "No RMA config for Alpina trustee"
|
||||
assert configs[0].get("connectorType") == "rma"
|
||||
|
||||
|
||||
class TestDemoBootstrapNeutralization:
|
||||
|
||||
def test_neutralizationConfigHappylife(self, db, mandateHappylife):
|
||||
from modules.features.neutralization.datamodelFeatureNeutralizer import DataNeutraliserConfig
|
||||
mid = mandateHappylife.get("id")
|
||||
instances = _getFeatureInstances(db, mid, "neutralization")
|
||||
assert instances
|
||||
iid = instances[0].get("id")
|
||||
configs = db.getRecordset(DataNeutraliserConfig, recordFilter={"featureInstanceId": iid})
|
||||
assert len(configs) >= 1, "No neutralization config for HappyLife"
|
||||
assert configs[0].get("enabled") is True
|
||||
|
||||
def test_neutralizationConfigAlpina(self, db, mandateAlpina):
|
||||
from modules.features.neutralization.datamodelFeatureNeutralizer import DataNeutraliserConfig
|
||||
mid = mandateAlpina.get("id")
|
||||
instances = _getFeatureInstances(db, mid, "neutralization")
|
||||
assert instances
|
||||
iid = instances[0].get("id")
|
||||
configs = db.getRecordset(DataNeutraliserConfig, recordFilter={"featureInstanceId": iid})
|
||||
assert len(configs) >= 1, "No neutralization config for Alpina"
|
||||
|
||||
|
||||
class TestDemoRemoveAndReload:
|
||||
|
||||
def test_removeAndReload(self, db, demoConfig):
|
||||
"""Remove all demo data, verify gone, then reload."""
|
||||
removeSummary = demoConfig.remove(db)
|
||||
assert len(removeSummary.get("errors", [])) == 0, f"Remove errors: {removeSummary['errors']}"
|
||||
|
||||
mandates = db.getRecordset(Mandate, recordFilter={"name": "happylife"})
|
||||
assert len(mandates) == 0, "HappyLife mandate should be gone after remove"
|
||||
|
||||
users = db.getRecordset(UserInDB, recordFilter={"username": "patrick.helvetia"})
|
||||
assert len(users) == 0, "User should be gone after remove"
|
||||
|
||||
loadSummary = demoConfig.load(db)
|
||||
assert len(loadSummary.get("errors", [])) == 0, f"Reload errors: {loadSummary['errors']}"
|
||||
|
||||
mandates = db.getRecordset(Mandate, recordFilter={"name": "happylife"})
|
||||
assert len(mandates) == 1, "HappyLife mandate should exist after reload"
|
||||
44
tests/demo/test_demo_data_files.py
Normal file
44
tests/demo/test_demo_data_files.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
"""
|
||||
T-DATA: Demo data files verification.
|
||||
|
||||
Ensures all expected demo data files exist in gateway/demoData/.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
_DEMO_DATA_ROOT = Path(__file__).resolve().parent.parent.parent / "demoData"
|
||||
|
||||
|
||||
class TestDemoDataStructure:
|
||||
|
||||
def test_rootExists(self):
|
||||
assert _DEMO_DATA_ROOT.exists(), f"demoData root not found: {_DEMO_DATA_ROOT}"
|
||||
|
||||
def test_invoicesNotEmpty(self):
|
||||
d = _DEMO_DATA_ROOT / "invoices"
|
||||
assert d.exists(), "invoices/ dir missing"
|
||||
files = [f for f in d.iterdir() if not f.name.startswith(".")]
|
||||
assert len(files) >= 1, f"invoices/ is empty: {list(d.iterdir())}"
|
||||
|
||||
def test_expensesNotEmpty(self):
|
||||
d = _DEMO_DATA_ROOT / "expenses"
|
||||
assert d.exists(), "expenses/ dir missing"
|
||||
files = [f for f in d.iterdir() if not f.name.startswith(".")]
|
||||
assert len(files) >= 1, f"expenses/ is empty: {list(d.iterdir())}"
|
||||
|
||||
def test_knowledgeBaseNotEmpty(self):
|
||||
d = _DEMO_DATA_ROOT / "knowledge-base"
|
||||
assert d.exists(), "knowledge-base/ dir missing"
|
||||
files = [f for f in d.iterdir() if not f.name.startswith(".")]
|
||||
assert len(files) >= 3, f"knowledge-base/ should have >=3 docs, found {len(files)}"
|
||||
|
||||
def test_neutralizerHasDossier(self):
|
||||
pdf = _DEMO_DATA_ROOT / "neutralizer" / "tenant-dossier.pdf"
|
||||
assert pdf.exists(), "tenant-dossier.pdf missing"
|
||||
assert pdf.stat().st_size > 500, "tenant-dossier.pdf too small"
|
||||
|
||||
def test_trusteeNotEmpty(self):
|
||||
d = _DEMO_DATA_ROOT / "trustee"
|
||||
assert d.exists(), "trustee/ dir missing"
|
||||
files = [f for f in d.iterdir() if not f.name.startswith(".")]
|
||||
assert len(files) >= 1, f"trustee/ is empty"
|
||||
36
tests/demo/test_demo_neutralization.py
Normal file
36
tests/demo/test_demo_neutralization.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
"""
|
||||
T-NEU: Neutralization config verification.
|
||||
|
||||
Verifies that neutralization is configured and enabled
|
||||
for both demo mandates.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from tests.demo.conftest import _getFeatureInstances
|
||||
|
||||
|
||||
class TestNeutralizationConfig:
|
||||
|
||||
@pytest.mark.parametrize("mandateFixture", ["mandateHappylife", "mandateAlpina"])
|
||||
def test_neutralizationEnabled(self, db, mandateFixture, request):
|
||||
"""Neutralization must be enabled for both mandates."""
|
||||
mandate = request.getfixturevalue(mandateFixture)
|
||||
mid = mandate.get("id")
|
||||
instances = _getFeatureInstances(db, mid, "neutralization")
|
||||
assert instances, f"No neutralization instance in {mandate.get('label')}"
|
||||
|
||||
from modules.features.neutralization.datamodelFeatureNeutralizer import DataNeutraliserConfig
|
||||
iid = instances[0].get("id")
|
||||
configs = db.getRecordset(DataNeutraliserConfig, recordFilter={"featureInstanceId": iid})
|
||||
assert configs, f"No neutralization config in {mandate.get('label')}"
|
||||
assert configs[0].get("enabled") is True, f"Neutralization not enabled in {mandate.get('label')}"
|
||||
|
||||
|
||||
class TestNeutralizationTestData:
|
||||
|
||||
def test_tenantDossierExists(self):
|
||||
"""The tenant-dossier.pdf must exist in demoData."""
|
||||
from pathlib import Path
|
||||
dossier = Path(__file__).resolve().parent.parent.parent / "demoData" / "neutralizer" / "tenant-dossier.pdf"
|
||||
assert dossier.exists(), f"tenant-dossier.pdf not found at {dossier}"
|
||||
assert dossier.stat().st_size > 500, "tenant-dossier.pdf seems too small"
|
||||
60
tests/demo/test_demo_uc1_trustee.py
Normal file
60
tests/demo/test_demo_uc1_trustee.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
"""
|
||||
T-UC1: Trustee — Spesenverarbeitung.
|
||||
|
||||
Verifies that the trustee feature instances are correctly configured
|
||||
with RMA accounting and that system workflow templates exist.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from tests.demo.conftest import _getFeatureInstances
|
||||
|
||||
|
||||
class TestTrusteeSetup:
|
||||
|
||||
def test_trusteeInstancesExist(self, db, mandateHappylife, mandateAlpina):
|
||||
"""Both mandates must have a trustee instance."""
|
||||
for mandate in [mandateHappylife, mandateAlpina]:
|
||||
mid = mandate.get("id")
|
||||
instances = _getFeatureInstances(db, mid, "trustee")
|
||||
assert len(instances) >= 1, f"No trustee in {mandate.get('label')}"
|
||||
|
||||
def test_rmaCredentialsEncrypted(self, db, mandateHappylife):
|
||||
"""RMA config must have non-empty encrypted credentials."""
|
||||
from modules.features.trustee.datamodelFeatureTrustee import TrusteeAccountingConfig
|
||||
mid = mandateHappylife.get("id")
|
||||
instances = _getFeatureInstances(db, mid, "trustee")
|
||||
iid = instances[0].get("id")
|
||||
configs = db.getRecordset(TrusteeAccountingConfig, recordFilter={"featureInstanceId": iid})
|
||||
assert configs
|
||||
enc = configs[0].get("encryptedConfig", "")
|
||||
assert enc and len(enc) > 10, "encryptedConfig should be a non-trivial encrypted blob"
|
||||
|
||||
def test_rmaCredentialsDecryptable(self, db, mandateHappylife):
|
||||
"""Encrypted RMA config must be decryptable and contain expected keys."""
|
||||
import json
|
||||
from modules.features.trustee.datamodelFeatureTrustee import TrusteeAccountingConfig
|
||||
from modules.shared.configuration import decryptValue
|
||||
mid = mandateHappylife.get("id")
|
||||
instances = _getFeatureInstances(db, mid, "trustee")
|
||||
iid = instances[0].get("id")
|
||||
configs = db.getRecordset(TrusteeAccountingConfig, recordFilter={"featureInstanceId": iid})
|
||||
enc = configs[0].get("encryptedConfig", "")
|
||||
plain = json.loads(decryptValue(enc, userId="system", keyName="accountingConfig"))
|
||||
assert "apiBaseUrl" in plain
|
||||
assert "clientName" in plain
|
||||
assert "apiKey" in plain
|
||||
assert plain["apiKey"], "apiKey should not be empty"
|
||||
|
||||
|
||||
class TestSystemWorkflowTemplates:
|
||||
|
||||
def test_systemTemplatesExist(self, db):
|
||||
"""System workflow templates should exist (created by system bootstrap, not demo config)."""
|
||||
from modules.features.graphicalEditor.datamodelFeatureGraphicalEditor import AutoWorkflow
|
||||
try:
|
||||
templates = db.getRecordset(AutoWorkflow, recordFilter={"isTemplate": True, "templateScope": "system"})
|
||||
except Exception:
|
||||
pytest.skip("AutoWorkflow table not accessible from app DB")
|
||||
return
|
||||
if len(templates) == 0:
|
||||
pytest.skip("No system workflow templates — run full system bootstrap first")
|
||||
24
tests/demo/test_demo_uc2_realestate.py
Normal file
24
tests/demo/test_demo_uc2_realestate.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
"""
|
||||
T-UC2: Immobilien — Machbarkeitsstudie.
|
||||
|
||||
Verifies that the workspace feature is available for the agent-based
|
||||
real estate demo (UC2 runs via workspace, not a dedicated realestate instance).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from tests.demo.conftest import _getFeatureInstances
|
||||
|
||||
|
||||
class TestRealEstateReadiness:
|
||||
|
||||
def test_workspaceInstanceHappylife(self, db, mandateHappylife):
|
||||
"""HappyLife must have a workspace instance for the agent demo."""
|
||||
mid = mandateHappylife.get("id")
|
||||
instances = _getFeatureInstances(db, mid, "workspace")
|
||||
assert len(instances) >= 1, "No workspace instance in HappyLife for UC2"
|
||||
|
||||
def test_workspaceInstanceAlpina(self, db, mandateAlpina):
|
||||
"""Alpina must have a workspace instance."""
|
||||
mid = mandateAlpina.get("id")
|
||||
instances = _getFeatureInstances(db, mid, "workspace")
|
||||
assert len(instances) >= 1, "No workspace instance in Alpina"
|
||||
37
tests/demo/test_demo_uc3_chatbot.py
Normal file
37
tests/demo/test_demo_uc3_chatbot.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
"""
|
||||
T-UC3: Knowledge Chatbot.
|
||||
|
||||
Verifies that the chatbot feature instance exists in HappyLife AG
|
||||
and that knowledge-base documents are available for upload.
|
||||
Note: The actual RAG demo runs via workspace, not the chatbot's own index.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from tests.demo.conftest import _getFeatureInstances
|
||||
|
||||
|
||||
class TestChatbotSetup:
|
||||
|
||||
def test_chatbotInstanceHappylife(self, db, mandateHappylife):
|
||||
"""HappyLife must have a chatbot instance."""
|
||||
mid = mandateHappylife.get("id")
|
||||
instances = _getFeatureInstances(db, mid, "chatbot")
|
||||
assert len(instances) >= 1, "No chatbot instance in HappyLife"
|
||||
|
||||
def test_chatbotNotInAlpina(self, db, mandateAlpina):
|
||||
"""Alpina should NOT have a chatbot instance."""
|
||||
mid = mandateAlpina.get("id")
|
||||
instances = _getFeatureInstances(db, mid, "chatbot")
|
||||
assert len(instances) == 0, "Alpina should not have chatbot"
|
||||
|
||||
|
||||
class TestKnowledgeBaseFiles:
|
||||
|
||||
def test_knowledgeBaseFilesExist(self):
|
||||
"""Knowledge-base documents must exist in demoData."""
|
||||
kbDir = Path(__file__).resolve().parent.parent.parent / "demoData" / "knowledge-base"
|
||||
assert kbDir.exists(), f"knowledge-base dir not found at {kbDir}"
|
||||
files = list(kbDir.iterdir())
|
||||
docs = [f for f in files if f.suffix in (".md", ".html", ".pdf", ".docx", ".txt")]
|
||||
assert len(docs) >= 3, f"Expected at least 3 knowledge-base docs, found {len(docs)}: {[f.name for f in docs]}"
|
||||
51
tests/demo/test_demo_uc4_i18n.py
Normal file
51
tests/demo/test_demo_uc4_i18n.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
"""
|
||||
T-UC4: Sprach-Deployment — Spanish (es).
|
||||
|
||||
Verifies that the i18n system is ready for the live demo:
|
||||
- Admin languages page is reachable
|
||||
- Spanish is available as a choice but NOT pre-installed
|
||||
- xx base set exists with entries
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestI18nReadiness:
|
||||
|
||||
def test_xxBaseSetExists(self, db):
|
||||
"""The xx (meta/base) language set must exist with entries."""
|
||||
try:
|
||||
from modules.datamodels.datamodelUiLanguage import UiLanguageSet
|
||||
sets = db.getRecordset(UiLanguageSet, recordFilter={"id": "xx"})
|
||||
assert sets, "xx base set not found — run i18n sync first"
|
||||
entries = sets[0].get("entries") or []
|
||||
assert len(entries) > 50, f"xx set has only {len(entries)} entries — expected 50+"
|
||||
except Exception as e:
|
||||
pytest.skip(f"i18n table not accessible: {e}")
|
||||
|
||||
def test_spanishNotPreInstalled(self, db):
|
||||
"""Spanish (es) must NOT be pre-installed — it will be created live."""
|
||||
try:
|
||||
from modules.datamodels.datamodelUiLanguage import UiLanguageSet
|
||||
sets = db.getRecordset(UiLanguageSet, recordFilter={"id": "es"})
|
||||
assert len(sets) == 0, "Spanish (es) is already installed — remove it before demo!"
|
||||
except Exception as e:
|
||||
pytest.skip(f"i18n table not accessible: {e}")
|
||||
|
||||
def test_germanSetExists(self, db):
|
||||
"""German (de) set must exist and be complete."""
|
||||
try:
|
||||
from modules.datamodels.datamodelUiLanguage import UiLanguageSet
|
||||
sets = db.getRecordset(UiLanguageSet, recordFilter={"id": "de"})
|
||||
assert sets, "German (de) set not found"
|
||||
except Exception as e:
|
||||
pytest.skip(f"i18n table not accessible: {e}")
|
||||
|
||||
def test_englishSetExists(self, db):
|
||||
"""English (en) set must exist."""
|
||||
try:
|
||||
from modules.datamodels.datamodelUiLanguage import UiLanguageSet
|
||||
sets = db.getRecordset(UiLanguageSet, recordFilter={"id": "en"})
|
||||
assert sets, "English (en) set not found"
|
||||
except Exception as e:
|
||||
pytest.skip(f"i18n table not accessible: {e}")
|
||||
Loading…
Reference in a new issue