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