From ab48e2e853ebaeebdf873e3d733ebc46cc551b93 Mon Sep 17 00:00:00 2001
From: patrick-motsch
Date: Tue, 10 Feb 2026 00:10:07 +0100
Subject: [PATCH] enhanced generic navigation tree
---
modules/datamodels/datamodelMembership.py | 2 +-
modules/datamodels/datamodelRbac.py | 2 +-
modules/datamodels/datamodelUam.py | 8 +++----
modules/interfaces/interfaceBootstrap.py | 29 +++++++++++++++++++++++
modules/interfaces/interfaceDbApp.py | 4 ++--
modules/routes/routeBilling.py | 2 +-
modules/routes/routeDataMandates.py | 4 ++--
modules/routes/routeInvitations.py | 6 ++---
modules/routes/routeSystem.py | 2 +-
9 files changed, 44 insertions(+), 15 deletions(-)
diff --git a/modules/datamodels/datamodelMembership.py b/modules/datamodels/datamodelMembership.py
index e100b23c..5e8b8814 100644
--- a/modules/datamodels/datamodelMembership.py
+++ b/modules/datamodels/datamodelMembership.py
@@ -28,7 +28,7 @@ class UserMandate(BaseModel):
)
mandateId: str = Field(
description="FK → Mandate.id (CASCADE DELETE)",
- json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/mandates/", "frontend_fk_display_field": "name"}
+ json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/mandates/", "frontend_fk_display_field": "label"}
)
enabled: bool = Field(
default=True,
diff --git a/modules/datamodels/datamodelRbac.py b/modules/datamodels/datamodelRbac.py
index 64ad56a4..978c3be6 100644
--- a/modules/datamodels/datamodelRbac.py
+++ b/modules/datamodels/datamodelRbac.py
@@ -55,7 +55,7 @@ class Role(BaseModel):
mandateId: Optional[str] = Field(
default=None,
description="FK → Mandate.id (CASCADE DELETE). Null = Global/Template role.",
- json_schema_extra={"frontend_type": "select", "frontend_readonly": True, "frontend_visible": True, "frontend_required": False, "frontend_fk_source": "/api/mandates/", "frontend_fk_display_field": "name"}
+ json_schema_extra={"frontend_type": "select", "frontend_readonly": True, "frontend_visible": True, "frontend_required": False, "frontend_fk_source": "/api/mandates/", "frontend_fk_display_field": "label"}
)
featureInstanceId: Optional[str] = Field(
default=None,
diff --git a/modules/datamodels/datamodelUam.py b/modules/datamodels/datamodelUam.py
index 6b7cdc06..22d94ebe 100644
--- a/modules/datamodels/datamodelUam.py
+++ b/modules/datamodels/datamodelUam.py
@@ -73,10 +73,10 @@ class Mandate(BaseModel):
description="Name of the mandate",
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
)
- description: Optional[str] = Field(
+ label: Optional[str] = Field(
default=None,
- description="Description of the mandate",
- json_schema_extra={"frontend_type": "textarea", "frontend_readonly": False, "frontend_required": False}
+ description="Display label of the mandate",
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False}
)
enabled: bool = Field(
default=True,
@@ -104,7 +104,7 @@ registerModelLabels(
{
"id": {"en": "ID", "de": "ID", "fr": "ID"},
"name": {"en": "Name", "de": "Name", "fr": "Nom"},
- "description": {"en": "Description", "de": "Beschreibung", "fr": "Description"},
+ "label": {"en": "Label", "de": "Label", "fr": "Libellé"},
"enabled": {"en": "Enabled", "de": "Aktiviert", "fr": "Activé"},
"isSystem": {"en": "System Mandate", "de": "System-Mandant", "fr": "Mandat système"},
},
diff --git a/modules/interfaces/interfaceBootstrap.py b/modules/interfaces/interfaceBootstrap.py
index 8a58f352..3f678a2b 100644
--- a/modules/interfaces/interfaceBootstrap.py
+++ b/modules/interfaces/interfaceBootstrap.py
@@ -51,6 +51,9 @@ def initBootstrap(db: DatabaseConnector) -> None:
# Initialize root mandate
mandateId = initRootMandate(db)
+ # Migrate existing mandate records: description -> label
+ _migrateMandateDescriptionToLabel(db)
+
# Initialize system role TEMPLATES (mandateId=None, isSystemRole=True)
initRoles(db)
@@ -276,6 +279,32 @@ def initRootMandate(db: DatabaseConnector) -> Optional[str]:
return mandateId
+def _migrateMandateDescriptionToLabel(db: DatabaseConnector) -> None:
+ """
+ Migration: Rename 'description' field to 'label' in all Mandate records.
+ Copies existing 'description' values to 'label' and removes the old field.
+ Safe to run multiple times (idempotent).
+ """
+ allMandates = db.getRecordset(Mandate)
+ migratedCount = 0
+ for mandateRecord in allMandates:
+ mandateId = mandateRecord.get("id")
+ hasDescription = "description" in mandateRecord and mandateRecord.get("description") is not None
+ hasLabel = "label" in mandateRecord and mandateRecord.get("label") is not None
+
+ if hasDescription and not hasLabel:
+ # Copy description to label
+ updateData = {"label": mandateRecord["description"]}
+ db.recordModify(Mandate, mandateId, updateData)
+ migratedCount += 1
+ logger.info(f"Migrated mandate {mandateId}: description -> label")
+
+ if migratedCount > 0:
+ logger.info(f"Migrated {migratedCount} mandate(s) from description to label")
+ else:
+ logger.debug("No mandate description->label migration needed")
+
+
def initAdminUser(db: DatabaseConnector, mandateId: Optional[str]) -> Optional[str]:
"""
Creates the Admin user if it doesn't exist.
diff --git a/modules/interfaces/interfaceDbApp.py b/modules/interfaces/interfaceDbApp.py
index 2ca8232b..d6e4c6c0 100644
--- a/modules/interfaces/interfaceDbApp.py
+++ b/modules/interfaces/interfaceDbApp.py
@@ -1444,7 +1444,7 @@ class AppObjects:
return Mandate(**filteredMandates[0])
- def createMandate(self, name: str, description: str = None, enabled: bool = True) -> Mandate:
+ def createMandate(self, name: str, label: str = None, enabled: bool = True) -> Mandate:
"""
Creates a new mandate if user has permission.
Automatically copies system template roles (admin, user, viewer) to the new mandate.
@@ -1453,7 +1453,7 @@ class AppObjects:
raise PermissionError("No permission to create mandates")
# Create mandate data using model
- mandateData = Mandate(name=name, description=description, enabled=enabled)
+ mandateData = Mandate(name=name, label=label, enabled=enabled)
# Create mandate record
createdRecord = self.db.recordCreate(Mandate, mandateData)
diff --git a/modules/routes/routeBilling.py b/modules/routes/routeBilling.py
index e785ed13..586cc6dd 100644
--- a/modules/routes/routeBilling.py
+++ b/modules/routes/routeBilling.py
@@ -355,7 +355,7 @@ def getBalanceForMandate(
from modules.interfaces.interfaceDbApp import getInterface as getAppInterface
appInterface = getAppInterface(ctx.user, mandateId=targetMandateId)
mandate = appInterface.getMandate(targetMandateId)
- mandateName = mandate.get("name", "") if mandate else ""
+ mandateName = (mandate.get("label") or mandate.get("name", "")) if mandate else ""
return BillingBalanceResponse(
mandateId=targetMandateId,
diff --git a/modules/routes/routeDataMandates.py b/modules/routes/routeDataMandates.py
index 8d2c4a2b..b74b6277 100644
--- a/modules/routes/routeDataMandates.py
+++ b/modules/routes/routeDataMandates.py
@@ -192,7 +192,7 @@ def create_mandate(
)
# Get optional fields with defaults
- description = mandateData.get('description')
+ label = mandateData.get('label')
enabled = mandateData.get('enabled', True)
appInterface = interfaceDbApp.getRootInterface()
@@ -200,7 +200,7 @@ def create_mandate(
# Create mandate
newMandate = appInterface.createMandate(
name=name,
- description=description,
+ label=label,
enabled=enabled
)
diff --git a/modules/routes/routeInvitations.py b/modules/routes/routeInvitations.py
index 01c395e2..2454db44 100644
--- a/modules/routes/routeInvitations.py
+++ b/modules/routes/routeInvitations.py
@@ -190,7 +190,7 @@ def create_invitation(
from modules.connectors.connectorMessagingEmail import ConnectorMessagingEmail
# Get mandate name for the email
mandate = rootInterface.getMandate(str(context.mandateId))
- mandateName = mandate.name if mandate else "PowerOn"
+ mandateName = (mandate.label or mandate.name) if mandate else "PowerOn"
emailConnector = ConnectorMessagingEmail()
emailSubject = f"Einladung zu {mandateName}"
@@ -249,7 +249,7 @@ def create_invitation(
# Get mandate name for notification
mandate = rootInterface.getMandate(str(context.mandateId))
- mandateName = mandate.mandateLabel if mandate and mandate.mandateLabel else "PowerOn"
+ mandateName = (mandate.label or mandate.name) if mandate else "PowerOn"
inviterName = context.user.fullName or context.user.username
createInvitationNotification(
@@ -529,7 +529,7 @@ def validate_invitation(
# Get mandate name
mandate = rootInterface.getMandate(str(mandateId)) if mandateId else None
if mandate:
- mandateName = mandate.name
+ mandateName = mandate.label or mandate.name
# Get role names
roleIds = invitation.roleIds or []
diff --git a/modules/routes/routeSystem.py b/modules/routes/routeSystem.py
index 3c8cdd3d..83f4ed85 100644
--- a/modules/routes/routeSystem.py
+++ b/modules/routes/routeSystem.py
@@ -153,7 +153,7 @@ def _buildDynamicBlock(
mandateId = str(instance.mandateId)
if mandateId not in mandatesMap:
mandate = rootInterface.getMandate(mandateId)
- mandateName = mandate.name if mandate and hasattr(mandate, 'name') else mandateId
+ mandateName = (mandate.label or mandate.name) if mandate else mandateId
mandatesMap[mandateId] = {
"id": mandateId,
"uiLabel": mandateName,