diff --git a/modules/interfaces/interfaceAppModel.py b/modules/interfaces/interfaceAppModel.py index be026a27..1a847e19 100644 --- a/modules/interfaces/interfaceAppModel.py +++ b/modules/interfaces/interfaceAppModel.py @@ -31,10 +31,39 @@ class ConnectionStatus(str, Enum): class Mandate(BaseModel, ModelMixin): """Data model for a mandate""" - id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the mandate") - name: str = Field(description="Name of the mandate") - language: str = Field(default="en", description="Default language of the mandate") - enabled: bool = Field(default=True, description="Indicates whether the mandate is enabled") + id: str = Field( + default_factory=lambda: str(uuid.uuid4()), + description="Unique ID of the mandate", + frontend_type="text", + frontend_readonly=True, + frontend_required=False + ) + name: str = Field( + description="Name of the mandate", + frontend_type="text", + frontend_readonly=False, + frontend_required=True + ) + language: str = Field( + default="en", + description="Default language of the mandate", + frontend_type="select", + frontend_readonly=False, + frontend_required=True, + frontend_options=[ + {"value": "de", "label": {"en": "Deutsch", "fr": "Allemand"}}, + {"value": "en", "label": {"en": "English", "fr": "Anglais"}}, + {"value": "fr", "label": {"en": "Français", "fr": "Français"}}, + {"value": "it", "label": {"en": "Italiano", "fr": "Italien"}} + ] + ) + enabled: bool = Field( + default=True, + description="Indicates whether the mandate is enabled", + frontend_type="checkbox", + frontend_readonly=False, + frontend_required=False + ) # Register labels for Mandate register_model_labels( @@ -50,20 +79,83 @@ register_model_labels( class UserConnection(BaseModel, ModelMixin): """Data model for a user's connection to an external service""" - id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the connection") - userId: str = Field(description="ID of the user this connection belongs to") - authority: AuthAuthority = Field(description="Authentication authority") - externalId: str = Field(description="User ID in the external system") - externalUsername: str = Field(description="Username in the external system") - externalEmail: Optional[EmailStr] = Field(None, description="Email in the external system") - status: ConnectionStatus = Field(default=ConnectionStatus.ACTIVE, description="Connection status") - connectedAt: float = Field(default_factory=get_utc_timestamp, description="When the connection was established (UTC timestamp in seconds)") - lastChecked: float = Field(default_factory=get_utc_timestamp, description="When the connection was last verified (UTC timestamp in seconds)") - expiresAt: Optional[float] = Field(None, description="When the connection expires (UTC timestamp in seconds)") - - def to_dict(self) -> Dict[str, Any]: - """Convert the model to a dictionary""" - return super().to_dict() + id: str = Field( + default_factory=lambda: str(uuid.uuid4()), + description="Unique ID of the connection", + frontend_type="text", + frontend_readonly=True, + frontend_required=False + ) + userId: str = Field( + description="ID of the user this connection belongs to", + frontend_type="text", + frontend_readonly=True, + frontend_required=False + ) + authority: AuthAuthority = Field( + description="Authentication authority", + frontend_type="select", + frontend_readonly=True, + frontend_required=False, + frontend_options=[ + {"value": "local", "label": {"en": "Local", "fr": "Local"}}, + {"value": "google", "label": {"en": "Google", "fr": "Google"}}, + {"value": "msft", "label": {"en": "Microsoft", "fr": "Microsoft"}} + ] + ) + externalId: str = Field( + description="User ID in the external system", + frontend_type="text", + frontend_readonly=True, + frontend_required=False + ) + externalUsername: str = Field( + description="Username in the external system", + frontend_type="text", + frontend_readonly=False, + frontend_required=False + ) + externalEmail: Optional[EmailStr] = Field( + None, + description="Email in the external system", + frontend_type="email", + frontend_readonly=False, + frontend_required=False + ) + status: ConnectionStatus = Field( + default=ConnectionStatus.ACTIVE, + description="Connection status", + frontend_type="select", + frontend_readonly=False, + frontend_required=False, + frontend_options=[ + {"value": "active", "label": {"en": "Active", "fr": "Actif"}}, + {"value": "inactive", "label": {"en": "Inactive", "fr": "Inactif"}}, + {"value": "expired", "label": {"en": "Expired", "fr": "Expiré"}}, + {"value": "pending", "label": {"en": "Pending", "fr": "En attente"}} + ] + ) + connectedAt: float = Field( + default_factory=get_utc_timestamp, + description="When the connection was established (UTC timestamp in seconds)", + frontend_type="timestamp", + frontend_readonly=True, + frontend_required=False + ) + lastChecked: float = Field( + default_factory=get_utc_timestamp, + description="When the connection was last verified (UTC timestamp in seconds)", + frontend_type="timestamp", + frontend_readonly=True, + frontend_required=False + ) + expiresAt: Optional[float] = Field( + None, + description="When the connection expires (UTC timestamp in seconds)", + frontend_type="timestamp", + frontend_readonly=True, + frontend_required=False + ) # Register labels for UserConnection register_model_labels( @@ -135,15 +227,84 @@ register_model_labels( class User(BaseModel, ModelMixin): """Data model for a user""" - id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the user") - username: str = Field(description="Username for login") - email: Optional[EmailStr] = Field(None, description="Email address of the user") - fullName: Optional[str] = Field(None, description="Full name of the user") - language: str = Field(default="en", description="Preferred language of the user") - enabled: bool = Field(default=True, description="Indicates whether the user is enabled") - privilege: UserPrivilege = Field(default=UserPrivilege.USER, description="Permission level") - authenticationAuthority: AuthAuthority = Field(default=AuthAuthority.LOCAL, description="Primary authentication authority") - mandateId: Optional[str] = Field(None, description="ID of the mandate this user belongs to") + id: str = Field( + default_factory=lambda: str(uuid.uuid4()), + description="Unique ID of the user", + frontend_type="text", + frontend_readonly=True, + frontend_required=False + ) + username: str = Field( + description="Username for login", + frontend_type="text", + frontend_readonly=False, + frontend_required=True + ) + email: Optional[EmailStr] = Field( + None, + description="Email address of the user", + frontend_type="email", + frontend_readonly=False, + frontend_required=True + ) + fullName: Optional[str] = Field( + None, + description="Full name of the user", + frontend_type="text", + frontend_readonly=False, + frontend_required=False + ) + language: str = Field( + default="en", + description="Preferred language of the user", + frontend_type="select", + frontend_readonly=False, + frontend_required=True, + frontend_options=[ + {"value": "de", "label": {"en": "Deutsch", "fr": "Allemand"}}, + {"value": "en", "label": {"en": "English", "fr": "Anglais"}}, + {"value": "fr", "label": {"en": "Français", "fr": "Français"}}, + {"value": "it", "label": {"en": "Italiano", "fr": "Italien"}} + ] + ) + enabled: bool = Field( + default=True, + description="Indicates whether the user is enabled", + frontend_type="checkbox", + frontend_readonly=False, + frontend_required=False + ) + privilege: UserPrivilege = Field( + default=UserPrivilege.USER, + description="Permission level", + frontend_type="select", + frontend_readonly=False, + frontend_required=True, + frontend_options=[ + {"value": "user", "label": {"en": "User", "fr": "Utilisateur"}}, + {"value": "admin", "label": {"en": "Admin", "fr": "Administrateur"}}, + {"value": "sysadmin", "label": {"en": "SysAdmin", "fr": "Administrateur système"}} + ] + ) + authenticationAuthority: AuthAuthority = Field( + default=AuthAuthority.LOCAL, + description="Primary authentication authority", + frontend_type="select", + frontend_readonly=True, + frontend_required=False, + frontend_options=[ + {"value": "local", "label": {"en": "Local", "fr": "Local"}}, + {"value": "google", "label": {"en": "Google", "fr": "Google"}}, + {"value": "msft", "label": {"en": "Microsoft", "fr": "Microsoft"}} + ] + ) + mandateId: Optional[str] = Field( + None, + description="ID of the mandate this user belongs to", + frontend_type="text", + frontend_readonly=True, + frontend_required=False + ) # Register labels for User register_model_labels( diff --git a/modules/interfaces/interfaceAppObjects.py b/modules/interfaces/interfaceAppObjects.py index 451335c4..a7cc91ed 100644 --- a/modules/interfaces/interfaceAppObjects.py +++ b/modules/interfaces/interfaceAppObjects.py @@ -284,15 +284,6 @@ class AppObjects: result = [] for conn_dict in connections: try: - # Convert string dates to datetime objects - for field in ['connectedAt', 'lastChecked', 'expiresAt']: - if field in conn_dict and conn_dict[field]: - try: - if isinstance(conn_dict[field], str): - conn_dict[field] = datetime.fromisoformat(conn_dict[field].replace('Z', '+00:00')) - except (ValueError, TypeError): - conn_dict[field] = None - # Create UserConnection object connection = UserConnection( id=conn_dict["id"], @@ -831,8 +822,8 @@ class AppObjects: logger.warning(f"No token found for connection: {connectionId}") return None - # Sort by creation date and get the latest - tokens.sort(key=lambda x: x.get("createdAt", ""), reverse=True) + # Sort by expiration date and get the latest (most recent expiration) + tokens.sort(key=lambda x: x.get("expiresAt", 0), reverse=True) latest_token = Token(**tokens[0]) # Check if token is expired @@ -905,6 +896,35 @@ class AppObjects: logger.error(f"Error deleting token for connection {connectionId}: {str(e)}") raise + def cleanupExpiredTokens(self) -> int: + """Clean up expired tokens for all connections, returns count of cleaned tokens""" + try: + from modules.shared.timezoneUtils import get_utc_timestamp + + current_time = get_utc_timestamp() + cleaned_count = 0 + + # Get all tokens + all_tokens = self.db.getRecordset("tokens", recordFilter={}) + + for token_data in all_tokens: + if token_data.get("expiresAt") and token_data.get("expiresAt") < current_time: + # Token is expired, delete it + self.db.recordDelete("tokens", token_data["id"]) + cleaned_count += 1 + logger.debug(f"Cleaned up expired token {token_data['id']} for connection {token_data.get('connectionId')}") + + # Clear cache to ensure fresh data + if cleaned_count > 0: + self._clearTableCache("tokens") + logger.info(f"Cleaned up {cleaned_count} expired tokens") + + return cleaned_count + + except Exception as e: + logger.error(f"Error cleaning up expired tokens: {str(e)}") + return 0 + def logout(self) -> None: """Logout current user - clear user context and tokens""" try: diff --git a/modules/interfaces/interfaceChatModel.py b/modules/interfaces/interfaceChatModel.py index 00966bc4..1e2363a5 100644 --- a/modules/interfaces/interfaceChatModel.py +++ b/modules/interfaces/interfaceChatModel.py @@ -465,17 +465,86 @@ register_model_labels( class ChatWorkflow(BaseModel, ModelMixin): """Data model for a chat workflow""" - id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key") - mandateId: str = Field(description="ID of the mandate this workflow belongs to") - status: str = Field(description="Current status of the workflow") - name: Optional[str] = Field(None, description="Name of the workflow") - currentRound: int = Field(description="Current round number") - lastActivity: float = Field(default_factory=get_utc_timestamp, description="Timestamp of last activity (UTC timestamp in seconds)") - startedAt: float = Field(default_factory=get_utc_timestamp, description="When the workflow started (UTC timestamp in seconds)") - logs: List[ChatLog] = Field(default_factory=list, description="Workflow logs") - messages: List[ChatMessage] = Field(default_factory=list, description="Messages in the workflow") - stats: Optional[ChatStat] = Field(None, description="Workflow statistics") - tasks: List[TaskItem] = Field(default_factory=list, description="List of tasks in the workflow") + id: str = Field( + default_factory=lambda: str(uuid.uuid4()), + description="Primary key", + frontend_type="text", + frontend_readonly=True, + frontend_required=False + ) + mandateId: str = Field( + description="ID of the mandate this workflow belongs to", + frontend_type="text", + frontend_readonly=True, + frontend_required=False + ) + status: str = Field( + description="Current status of the workflow", + frontend_type="select", + frontend_readonly=False, + frontend_required=False, + frontend_options=[ + {"value": "running", "label": {"en": "Running", "fr": "En cours"}}, + {"value": "completed", "label": {"en": "Completed", "fr": "Terminé"}}, + {"value": "stopped", "label": {"en": "Stopped", "fr": "Arrêté"}}, + {"value": "error", "label": {"en": "Error", "fr": "Erreur"}} + ] + ) + name: Optional[str] = Field( + None, + description="Name of the workflow", + frontend_type="text", + frontend_readonly=False, + frontend_required=True + ) + currentRound: int = Field( + description="Current round number", + frontend_type="integer", + frontend_readonly=True, + frontend_required=False + ) + lastActivity: float = Field( + default_factory=get_utc_timestamp, + description="Timestamp of last activity (UTC timestamp in seconds)", + frontend_type="timestamp", + frontend_readonly=True, + frontend_required=False + ) + startedAt: float = Field( + default_factory=get_utc_timestamp, + description="When the workflow started (UTC timestamp in seconds)", + frontend_type="timestamp", + frontend_readonly=True, + frontend_required=False + ) + logs: List[ChatLog] = Field( + default_factory=list, + description="Workflow logs", + frontend_type="text", + frontend_readonly=True, + frontend_required=False + ) + messages: List[ChatMessage] = Field( + default_factory=list, + description="Messages in the workflow", + frontend_type="text", + frontend_readonly=True, + frontend_required=False + ) + stats: Optional[ChatStat] = Field( + None, + description="Workflow statistics", + frontend_type="text", + frontend_readonly=True, + frontend_required=False + ) + tasks: List[TaskItem] = Field( + default_factory=list, + description="List of tasks in the workflow", + frontend_type="text", + frontend_readonly=True, + frontend_required=False + ) # Register labels for ChatWorkflow register_model_labels( diff --git a/modules/interfaces/interfaceChatObjects.py b/modules/interfaces/interfaceChatObjects.py index 847c150f..f5f27d3e 100644 --- a/modules/interfaces/interfaceChatObjects.py +++ b/modules/interfaces/interfaceChatObjects.py @@ -1335,7 +1335,7 @@ class ChatObjects: retryCount=createdAction.get("retryCount", 0), retryMax=createdAction.get("retryMax", 3), processingTime=createdAction.get("processingTime"), - timestamp=datetime.fromtimestamp(float(createdAction.get("timestamp", get_utc_timestamp()))), + timestamp=float(createdAction.get("timestamp", get_utc_timestamp())), result=createdAction.get("result"), resultDocuments=createdAction.get("resultDocuments", []) ) diff --git a/modules/interfaces/interfaceComponentModel.py b/modules/interfaces/interfaceComponentModel.py index b5b0b55d..3cc19dd8 100644 --- a/modules/interfaces/interfaceComponentModel.py +++ b/modules/interfaces/interfaceComponentModel.py @@ -16,13 +16,50 @@ from modules.shared.timezoneUtils import get_utc_timestamp class FileItem(BaseModel, ModelMixin): """Data model for a file item""" - id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key") - mandateId: str = Field(description="ID of the mandate this file belongs to") - filename: str = Field(description="Name of the file") - mimeType: str = Field(description="MIME type of the file") - fileHash: str = Field(description="Hash of the file") - fileSize: int = Field(description="Size of the file in bytes") - creationDate: float = Field(default_factory=get_utc_timestamp, description="Date when the file was created (UTC timestamp in seconds)") + id: str = Field( + default_factory=lambda: str(uuid.uuid4()), + description="Primary key", + frontend_type="text", + frontend_readonly=True, + frontend_required=False + ) + mandateId: str = Field( + description="ID of the mandate this file belongs to", + frontend_type="text", + frontend_readonly=True, + frontend_required=False + ) + filename: str = Field( + description="Name of the file", + frontend_type="text", + frontend_readonly=False, + frontend_required=True + ) + mimeType: str = Field( + description="MIME type of the file", + frontend_type="text", + frontend_readonly=True, + frontend_required=False + ) + fileHash: str = Field( + description="Hash of the file", + frontend_type="text", + frontend_readonly=True, + frontend_required=False + ) + fileSize: int = Field( + description="Size of the file in bytes", + frontend_type="integer", + frontend_readonly=True, + frontend_required=False + ) + creationDate: float = Field( + default_factory=get_utc_timestamp, + description="Date when the file was created (UTC timestamp in seconds)", + frontend_type="timestamp", + frontend_readonly=True, + frontend_required=False + ) def to_dict(self) -> Dict[str, Any]: """Convert model to dictionary""" @@ -94,10 +131,31 @@ register_model_labels( class Prompt(BaseModel, ModelMixin): """Data model for a prompt""" - id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key") - mandateId: str = Field(description="ID of the mandate this prompt belongs to") - content: str = Field(description="Content of the prompt") - name: str = Field(description="Name of the prompt") + id: str = Field( + default_factory=lambda: str(uuid.uuid4()), + description="Primary key", + frontend_type="text", + frontend_readonly=True, + frontend_required=False + ) + mandateId: str = Field( + description="ID of the mandate this prompt belongs to", + frontend_type="text", + frontend_readonly=True, + frontend_required=False + ) + content: str = Field( + description="Content of the prompt", + frontend_type="textarea", + frontend_readonly=False, + frontend_required=True + ) + name: str = Field( + description="Name of the prompt", + frontend_type="text", + frontend_readonly=False, + frontend_required=True + ) # Register labels for Prompt register_model_labels( diff --git a/modules/routes/routeSecurityGoogle.py b/modules/routes/routeSecurityGoogle.py index 1d7d47b7..bfa80f4e 100644 --- a/modules/routes/routeSecurityGoogle.py +++ b/modules/routes/routeSecurityGoogle.py @@ -512,7 +512,7 @@ async def refresh_token( appInterface.deleteTokenByConnectionId(google_connection.id) # Update the connection's expiration time - google_connection.expiresAt = datetime.fromtimestamp(refreshed_token.expiresAt) + google_connection.expiresAt = float(refreshed_token.expiresAt) google_connection.lastChecked = get_utc_timestamp() google_connection.status = ConnectionStatus.ACTIVE diff --git a/modules/routes/routeSecurityMsft.py b/modules/routes/routeSecurityMsft.py index 8737b9f7..90a957c9 100644 --- a/modules/routes/routeSecurityMsft.py +++ b/modules/routes/routeSecurityMsft.py @@ -443,6 +443,31 @@ async def logout( detail=f"Failed to logout: {str(e)}" ) +@router.post("/cleanup") +@limiter.limit("5/minute") +async def cleanup_expired_tokens( + request: Request, + currentUser: User = Depends(getCurrentUser) +) -> Dict[str, Any]: + """Clean up expired tokens for the current user""" + try: + appInterface = getInterface(currentUser) + + # Clean up expired tokens + cleaned_count = appInterface.cleanupExpiredTokens() + + return { + "message": f"Cleanup completed successfully", + "tokens_cleaned": cleaned_count + } + + except Exception as e: + logger.error(f"Error cleaning up expired tokens: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to cleanup expired tokens: {str(e)}" + ) + @router.post("/refresh") @limiter.limit("10/minute") async def refresh_token( @@ -473,7 +498,8 @@ async def refresh_token( logger.debug(f"Found Microsoft connection: {msft_connection.id}, status={msft_connection.status}") # Get the token for this specific connection using the new method - current_token = appInterface.getTokenForConnection(msft_connection.id, auto_refresh=False) + # Enable auto-refresh to handle expired tokens gracefully + current_token = appInterface.getTokenForConnection(msft_connection.id, auto_refresh=True) if not current_token: raise HTTPException( @@ -494,7 +520,7 @@ async def refresh_token( appInterface.deleteTokenByConnectionId(msft_connection.id) # Update the connection's expiration time - msft_connection.expiresAt = datetime.fromtimestamp(refreshed_token.expiresAt) + msft_connection.expiresAt = float(refreshed_token.expiresAt) msft_connection.lastChecked = get_utc_timestamp() msft_connection.status = ConnectionStatus.ACTIVE diff --git a/modules/shared/attributeUtils.py b/modules/shared/attributeUtils.py index a4624109..1b690383 100644 --- a/modules/shared/attributeUtils.py +++ b/modules/shared/attributeUtils.py @@ -16,7 +16,7 @@ class ModelMixin: """ Convert a Pydantic model to a dictionary. Handles both Pydantic v1 and v2. - Properly serializes datetime fields to ISO format strings. + All timestamp fields remain as float values. Returns: Dict[str, Any]: Dictionary representation of the model @@ -27,42 +27,10 @@ class ModelMixin: else: data: Dict[str, Any] = self.dict() # Pydantic v1 - # Convert datetime fields to ISO format strings - for key, value in data.items(): - if isinstance(value, datetime): - data[key] = value.isoformat() - elif isinstance(value, (int, float)) and self._is_timestamp_field(key): - # Handle timestamp fields based on field metadata - try: - data[key] = datetime.fromtimestamp(value).isoformat() - except (ValueError, TypeError): - # If conversion fails, keep the original value - pass - - return data - - def _is_timestamp_field(self, field_name: str) -> bool: - """ - Check if a field is a timestamp field based on field metadata. - Looks for 'UTC timestamp' in the field description. - """ - try: - # Get field info from Pydantic model - if hasattr(self, 'model_fields'): - # Pydantic v2 - field_info = self.model_fields.get(field_name) - if field_info and field_info.description: - return 'UTC timestamp' in field_info.description - elif hasattr(self, '__fields__'): - # Pydantic v1 - field_info = self.__fields__.get(field_name) - if field_info and field_info.field_info and field_info.field_info.description: - return 'UTC timestamp' in field_info.field_info.description - except Exception: - pass + # All fields (including timestamps) remain in their original format + # No conversions needed - timestamps are already float - # Fallback: return False for safety - return False + return data @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'ModelMixin': @@ -89,6 +57,12 @@ class AttributeDefinition(BaseModel, ModelMixin): options: Optional[List[Any]] = None validation: Optional[Dict[str, Any]] = None ui: Optional[Dict[str, Any]] = None + # New frontend metadata fields + readonly: bool = False + editable: bool = True + visible: bool = True + order: int = 0 + placeholder: Optional[str] = None # Global registry for model labels MODEL_LABELS: Dict[str, Dict[str, Dict[str, str]]] = {} @@ -194,30 +168,92 @@ def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguag if hasattr(modelClass, 'model_fields'): # Pydantic v2 fields = modelClass.model_fields for name, field in fields.items(): + # Extract frontend metadata from field info + field_info = field.field_info if hasattr(field, 'field_info') else None + # Check both direct attributes and extra field for frontend metadata + frontend_type = None + frontend_readonly = False + frontend_required = field.is_required() + frontend_options = None + + if field_info: + # Try direct attributes first + frontend_type = getattr(field_info, 'frontend_type', None) + frontend_readonly = getattr(field_info, 'frontend_readonly', False) + frontend_required = getattr(field_info, 'frontend_required', frontend_required) + frontend_options = getattr(field_info, 'frontend_options', None) + + # If not found, check extra field + if hasattr(field_info, 'extra') and field_info.extra: + if frontend_type is None: + frontend_type = field_info.extra.get('frontend_type') + if not frontend_readonly: + frontend_readonly = field_info.extra.get('frontend_readonly', False) + if frontend_required == field.is_required(): # Only override if we didn't get it from direct attribute + frontend_required = field_info.extra.get('frontend_required', frontend_required) + if frontend_options is None: + frontend_options = field_info.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)) + attributes.append({ "name": name, - "type": field.annotation.__name__ if hasattr(field.annotation, "__name__") else str(field.annotation), - "required": field.is_required() if hasattr(field, "is_required") else True, + "type": field_type, + "required": frontend_required, "description": field.description if hasattr(field, "description") else "", "label": labels.get(name, name), "placeholder": f"Please enter {labels.get(name, name)}", - "editable": True, + "editable": not frontend_readonly, "visible": True, - "order": len(attributes) + "order": len(attributes), + "readonly": frontend_readonly, + "options": frontend_options }) else: # Pydantic v1 fields = modelClass.__fields__ for name, field in fields.items(): + # Extract frontend metadata from field info + field_info = field.field_info if hasattr(field, 'field_info') else None + # Check both direct attributes and extra field for frontend metadata + frontend_type = None + frontend_readonly = False + frontend_required = field.required + frontend_options = None + + if field_info: + # Try direct attributes first + frontend_type = getattr(field_info, 'frontend_type', None) + frontend_readonly = getattr(field_info, 'frontend_readonly', False) + frontend_required = getattr(field_info, 'frontend_required', frontend_required) + frontend_options = getattr(field_info, 'frontend_options', None) + + # If not found, check extra field + if hasattr(field_info, 'extra') and field_info.extra: + if frontend_type is None: + frontend_type = field_info.extra.get('frontend_type') + if not frontend_readonly: + frontend_readonly = field_info.extra.get('frontend_readonly', False) + if frontend_required == field.required: # Only override if we didn't get it from direct attribute + frontend_required = field_info.extra.get('frontend_required', frontend_required) + if frontend_options is None: + frontend_options = field_info.extra.get('frontend_options') + + # Use frontend type if available, otherwise fall back to Python type + field_type = frontend_type if frontend_type else (field.type_.__name__ if hasattr(field.type_, "__name__") else str(field.type_)) + attributes.append({ "name": name, - "type": field.type_.__name__ if hasattr(field.type_, "__name__") else str(field.type_), - "required": field.required, + "type": field_type, + "required": frontend_required, "description": field.field_info.description if hasattr(field.field_info, "description") else "", "label": labels.get(name, name), "placeholder": f"Please enter {labels.get(name, name)}", - "editable": True, + "editable": not frontend_readonly, "visible": True, - "order": len(attributes) + "order": len(attributes), + "readonly": frontend_readonly, + "options": frontend_options }) return { diff --git a/notes/readme.md b/notes/readme.md index 47f21832..14233338 100644 --- a/notes/readme.md +++ b/notes/readme.md @@ -92,6 +92,7 @@ git remote set-url origin https://valueon@github.com/valueonag/gateway git remote set-url origin https://valueon@github.com/valueonag/frontend_agents git remote set-url origin https://valueon@github.com/valueonag/wiki git remote set-url origin https://valueon@github.com/valueonag/customer-svbe +git remote set-url origin https://valueon@github.com/valueonag/customer-althaus ### git delete workflow runs (cleanup)