From 72f5fbde4690141458fc7f55fd8fc7b8b23079dc Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Mon, 8 Dec 2025 00:13:26 +0100
Subject: [PATCH] added attribute types: TextMultilingual, multiselect
---
modules/datamodels/datamodelRbac.py | 5 ++-
modules/datamodels/datamodelUtils.py | 51 ++++++++++++++++++++++++-
modules/features/options/mainOptions.py | 12 +++++-
modules/shared/attributeUtils.py | 31 ++++++++++-----
4 files changed, 84 insertions(+), 15 deletions(-)
diff --git a/modules/datamodels/datamodelRbac.py b/modules/datamodels/datamodelRbac.py
index 7fcfb6c4..96f7ef55 100644
--- a/modules/datamodels/datamodelRbac.py
+++ b/modules/datamodels/datamodelRbac.py
@@ -5,6 +5,7 @@ from typing import Optional, Dict
from enum import Enum
from pydantic import BaseModel, Field
from modules.shared.attributeUtils import registerModelLabels
+from modules.datamodels.datamodelUtils import TextMultilingual
from modules.datamodels.datamodelUam import AccessLevel
@@ -26,9 +27,9 @@ class Role(BaseModel):
description="Unique role label identifier (e.g., 'admin', 'user', 'viewer')",
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
)
- description: Dict[str, str] = Field(
+ description: TextMultilingual = Field(
description="Role description in multiple languages",
- json_schema_extra={"frontend_type": "object", "frontend_readonly": False, "frontend_required": True}
+ json_schema_extra={"frontend_type": "multilingual", "frontend_readonly": False, "frontend_required": True}
)
isSystemRole: bool = Field(
False,
diff --git a/modules/datamodels/datamodelUtils.py b/modules/datamodels/datamodelUtils.py
index 4f1c69c2..3ff5d3fa 100644
--- a/modules/datamodels/datamodelUtils.py
+++ b/modules/datamodels/datamodelUtils.py
@@ -1,6 +1,7 @@
-"""Utility datamodels: Prompt."""
+"""Utility datamodels: Prompt, TextMultilingual."""
-from pydantic import BaseModel, Field
+from typing import Dict, Optional
+from pydantic import BaseModel, Field, field_validator
from modules.shared.attributeUtils import registerModelLabels
import uuid
@@ -22,3 +23,49 @@ registerModelLabels(
)
+class TextMultilingual(BaseModel):
+ """
+ Multilingual text field supporting multiple languages.
+ Default languages: en (English), ge (German), fr (French), it (Italian)
+ English (en) is the default/required language.
+ """
+ en: str = Field(description="English text (default language, required)")
+ ge: Optional[str] = Field(None, description="German text")
+ fr: Optional[str] = Field(None, description="French text")
+ it: Optional[str] = Field(None, description="Italian text")
+
+ @field_validator('en')
+ @classmethod
+ def validate_en_required(cls, v):
+ """Ensure English text is not empty"""
+ if not v or not v.strip():
+ raise ValueError("English text (en) is required and cannot be empty")
+ return v
+
+ def model_dump(self, **kwargs) -> Dict[str, str]:
+ """Return as dictionary, filtering out None values"""
+ result = {}
+ for lang in ['en', 'ge', 'fr', 'it']:
+ value = getattr(self, lang, None)
+ if value is not None:
+ result[lang] = value
+ return result
+
+ @classmethod
+ def from_dict(cls, data: Dict[str, str]) -> 'TextMultilingual':
+ """Create TextMultilingual from dictionary"""
+ return cls(
+ en=data.get('en', ''),
+ ge=data.get('ge'),
+ fr=data.get('fr'),
+ it=data.get('it')
+ )
+
+ def get_text(self, lang: str = 'en') -> str:
+ """Get text for a specific language, fallback to English if not available"""
+ value = getattr(self, lang, None)
+ if value:
+ return value
+ return self.en # Fallback to English
+
+
diff --git a/modules/features/options/mainOptions.py b/modules/features/options/mainOptions.py
index 41ef5db2..d05b3bc1 100644
--- a/modules/features/options/mainOptions.py
+++ b/modules/features/options/mainOptions.py
@@ -64,7 +64,17 @@ def getOptions(optionsName: str, currentUser: Optional[User] = None) -> List[Dic
options = []
for role in roles:
# Use English description as label, fallback to roleLabel
- label = role.description.get("en", role.roleLabel) if isinstance(role.description, dict) else role.roleLabel
+ # Handle TextMultilingual object
+ if hasattr(role.description, 'get_text'):
+ # TextMultilingual object
+ label = role.description.get_text('en')
+ elif isinstance(role.description, dict):
+ # Dict format (backward compatibility)
+ label = role.description.get("en", role.roleLabel)
+ else:
+ # Fallback to roleLabel
+ label = role.roleLabel
+
options.append({
"value": role.roleLabel,
"label": label
diff --git a/modules/shared/attributeUtils.py b/modules/shared/attributeUtils.py
index 9116d330..74aeee10 100644
--- a/modules/shared/attributeUtils.py
+++ b/modules/shared/attributeUtils.py
@@ -166,16 +166,27 @@ def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguag
if frontend_options is None and "frontend_options" in json_extra:
frontend_options = json_extra.get("frontend_options")
- # Use frontend type if available, otherwise fall back to Python type
- field_type = (
- frontend_type
- if frontend_type
- else (
- field.annotation.__name__
- if hasattr(field.annotation, "__name__")
- else str(field.annotation)
- )
- )
+ # Use frontend type if available, otherwise detect from Python type
+ if frontend_type:
+ field_type = frontend_type
+ else:
+ # Check if it's TextMultilingual type
+ annotation_str = str(field.annotation)
+ # Check both the module path and class name for TextMultilingual
+ if ('TextMultilingual' in annotation_str or
+ (hasattr(field.annotation, '__name__') and field.annotation.__name__ == 'TextMultilingual') or
+ 'datamodelUtils.TextMultilingual' in annotation_str or
+ 'datamodels.datamodelUtils.TextMultilingual' in annotation_str):
+ field_type = 'multilingual'
+ elif hasattr(field.annotation, "__name__"):
+ annotation_name = field.annotation.__name__
+ # Check if it's a Dict type (for JSON/object fields)
+ if annotation_name == 'Dict' or annotation_str.startswith('typing.Dict') or annotation_str.startswith('Dict['):
+ field_type = 'object' # Will be rendered as textarea for JSON editing
+ else:
+ field_type = annotation_name
+ else:
+ field_type = str(field.annotation)
# Extract default value from field
# In Pydantic v2, FieldInfo has a 'default' attribute