# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ View models for the /api/attributes/ endpoint. These extend base DB models with computed / enriched fields that the gateway adds at response time (JOINs, aggregations, synthetics). They are NEVER used for DB operations — only for ``getModelAttributeDefinitions()`` so the frontend can resolve column types via ``resolveColumnTypes`` without hardcoding. Naming convention: ``{BaseModel}View``. ``getModelClasses()`` in ``attributeUtils.py`` auto-discovers every ``datamodel*.py`` under ``modules/datamodels/`` — so placing them here is sufficient for registration. """ from typing import Optional, List from pydantic import Field from modules.datamodels.datamodelBase import MODEL_REGISTRY, PowerOnModel from modules.datamodels.datamodelMembership import UserMandate, FeatureAccess from modules.datamodels.datamodelBilling import BillingTransaction from modules.datamodels.datamodelSubscription import MandateSubscription from modules.datamodels.datamodelUiLanguage import UiLanguageSet from modules.datamodels.datamodelRbac import Role from modules.features.neutralization.datamodelFeatureNeutralizer import DataNeutralizerAttributes from modules.shared.i18nRegistry import i18nModel # ============================================================================ # Punkt 1a: UserMandate + enriched user fields # ============================================================================ @i18nModel("Benutzer-Mandant (Ansicht)") class UserMandateView(UserMandate): """UserMandate erweitert um aufgeloeste Benutzerfelder und Rollenlabels.""" username: Optional[str] = Field( default=None, description="Username (resolved from userId)", json_schema_extra={"label": "Benutzername", "frontend_type": "text", "frontend_readonly": True}, ) email: Optional[str] = Field( default=None, description="E-Mail address (resolved from userId)", json_schema_extra={"label": "E-Mail", "frontend_type": "text", "frontend_readonly": True}, ) fullName: Optional[str] = Field( default=None, description="Full name (resolved from userId)", json_schema_extra={"label": "Vollstaendiger Name", "frontend_type": "text", "frontend_readonly": True}, ) roleLabels: Optional[List[str]] = Field( default=None, description="Role labels (resolved from junction table)", json_schema_extra={"label": "Rollen", "frontend_type": "text", "frontend_readonly": True}, ) # ============================================================================ # Punkt 1b: FeatureAccess + enriched user fields # ============================================================================ @i18nModel("Feature-Zugang (Ansicht)") class FeatureAccessView(FeatureAccess): """FeatureAccess erweitert um aufgeloeste Benutzerfelder und Rollenlabels.""" username: Optional[str] = Field( default=None, description="Username (resolved from userId)", json_schema_extra={"label": "Benutzername", "frontend_type": "text", "frontend_readonly": True}, ) email: Optional[str] = Field( default=None, description="E-Mail address (resolved from userId)", json_schema_extra={"label": "E-Mail", "frontend_type": "text", "frontend_readonly": True}, ) fullName: Optional[str] = Field( default=None, description="Full name (resolved from userId)", json_schema_extra={"label": "Vollstaendiger Name", "frontend_type": "text", "frontend_readonly": True}, ) roleLabels: Optional[List[str]] = Field( default=None, description="Role labels (resolved from junction table)", json_schema_extra={"label": "Rollen", "frontend_type": "text", "frontend_readonly": True}, ) # ============================================================================ # Punkt 1d: BillingTransaction + enriched mandate/user names # ============================================================================ @i18nModel("Transaktion (Ansicht)") class BillingTransactionView(BillingTransaction): """BillingTransaction erweitert um aufgeloeste Mandanten-/Benutzernamen.""" mandateName: Optional[str] = Field( default=None, description="Mandate name (resolved from accountId/mandateId)", json_schema_extra={"label": "Mandant", "frontend_type": "text", "frontend_readonly": True}, ) userName: Optional[str] = Field( default=None, description="User name (resolved from createdByUserId)", json_schema_extra={"label": "Benutzer", "frontend_type": "text", "frontend_readonly": True}, ) # ============================================================================ # Punkt 3a: MandateSubscription + aggregated fields # ============================================================================ @i18nModel("Abonnement (Ansicht)") class MandateSubscriptionView(MandateSubscription): """MandateSubscription erweitert um aggregierte Laufzeitwerte.""" mandateName: Optional[str] = Field( default=None, description="Mandate name (resolved from mandateId)", json_schema_extra={"label": "Mandant", "frontend_type": "text", "frontend_readonly": True}, ) planTitle: Optional[str] = Field( default=None, description="Plan title (resolved from planKey)", json_schema_extra={"label": "Plan", "frontend_type": "text", "frontend_readonly": True}, ) activeUsers: Optional[int] = Field( default=None, description="Number of active users in the mandate", json_schema_extra={"label": "Benutzer", "frontend_type": "number", "frontend_readonly": True}, ) activeInstances: Optional[int] = Field( default=None, description="Number of active feature instances in the mandate", json_schema_extra={"label": "Module", "frontend_type": "number", "frontend_readonly": True}, ) monthlyRevenueCHF: Optional[float] = Field( default=None, description="Calculated monthly revenue in CHF", json_schema_extra={"label": "Umsatz pro Monat", "frontend_type": "number", "frontend_readonly": True}, ) # ============================================================================ # Punkt 3b: UiLanguageSet + computed counts # ============================================================================ @i18nModel("Sprachset (Ansicht)") class UiLanguageSetView(UiLanguageSet): """UiLanguageSet erweitert um berechnete Uebersetzungszaehler.""" uiCount: Optional[int] = Field( default=None, description="Number of UI translation entries", json_schema_extra={"label": "UI", "frontend_type": "number", "frontend_readonly": True}, ) gatewayCount: Optional[int] = Field( default=None, description="Number of gateway/API translation entries", json_schema_extra={"label": "API", "frontend_type": "number", "frontend_readonly": True}, ) entriesCount: Optional[int] = Field( default=None, description="Total number of translation entries", json_schema_extra={"label": "Gesamt", "frontend_type": "number", "frontend_readonly": True}, ) # ============================================================================ # Punkt 1c: DataNeutralizerAttributes + enriched fields # # DataNeutralizerAttributes extends BaseModel (not PowerOnModel), so its # subclass does NOT auto-register in MODEL_REGISTRY. We register manually. # ============================================================================ @i18nModel("Neutralisierungs-Zuordnung (Ansicht)") class DataNeutralizerAttributesView(DataNeutralizerAttributes): """DataNeutralizerAttributes erweitert um synthetische/aufgeloeste Felder.""" placeholder: Optional[str] = Field( default=None, description="Synthetic placeholder string [patternType.id]", json_schema_extra={"label": "Platzhalter", "frontend_type": "text", "frontend_readonly": True}, ) username: Optional[str] = Field( default=None, description="Username (resolved from userId)", json_schema_extra={"label": "Benutzer", "frontend_type": "text", "frontend_readonly": True}, ) instanceLabel: Optional[str] = Field( default=None, description="Feature instance label (resolved from featureInstanceId)", json_schema_extra={"label": "Feature-Instanz", "frontend_type": "text", "frontend_readonly": True}, ) # Manual registration for non-PowerOnModel view MODEL_REGISTRY["DataNeutralizerAttributesView"] = DataNeutralizerAttributesView # type: ignore[assignment] # ============================================================================ # Role view — admin RBAC list with computed `scopeType` + `userCount` # # `scopeType` is computed in the route from (mandateId, isSystemRole). Exposed # here as a pure `select` field so the frontend renders the user-facing label # from `frontend_options` (no hardcoded mapping in the page). # ============================================================================ @i18nModel("Rolle (Ansicht)") class RoleView(Role): """Role extended with computed scope information for the admin UI.""" scopeType: Optional[str] = Field( default=None, description="Computed scope: 'system' (template), 'global', or 'mandate'.", json_schema_extra={ "label": "Geltungsbereich", "frontend_type": "select", "frontend_readonly": True, "frontend_required": False, "frontend_options": [ {"value": "system", "label": "System-Template"}, {"value": "global", "label": "Template"}, {"value": "mandate", "label": "Mandant"}, ], }, ) userCount: Optional[int] = Field( default=None, description="Number of users assigned to this role (via UserMandateRole).", json_schema_extra={ "label": "Benutzer", "frontend_type": "number", "frontend_readonly": True, "frontend_required": False, }, ) # ============================================================================ # Automation Workflow — dashboard view with synthesized fields # ============================================================================ from modules.features.graphicalEditor.datamodelFeatureGraphicalEditor import AutoWorkflow @i18nModel("Workflow (Ansicht)") class Automation2WorkflowView(AutoWorkflow): """AutoWorkflow extended with computed dashboard fields. Used exclusively for /api/attributes/ so the frontend can resolve column types for the workflow dashboard table (FormGeneratorTable). """ sysCreatedAt: Optional[float] = Field( default=None, description="Record creation timestamp (UTC)", json_schema_extra={ "label": "Erstellt", "frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False, }, ) lastStartedAt: Optional[float] = Field( default=None, description="Timestamp of the most recent workflow run start", json_schema_extra={ "label": "Zuletzt gestartet", "frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False, }, ) runCount: Optional[int] = Field( default=None, description="Total number of runs for this workflow", json_schema_extra={ "label": "Laeufe", "frontend_type": "number", "frontend_readonly": True, "frontend_required": False, }, ) mandateLabel: Optional[str] = Field( default=None, description="Mandate name (resolved from mandateId)", json_schema_extra={"label": "Mandant", "frontend_type": "text", "frontend_readonly": True}, ) instanceLabel: Optional[str] = Field( default=None, description="Feature instance label (resolved from featureInstanceId)", json_schema_extra={"label": "Feature-Instanz", "frontend_type": "text", "frontend_readonly": True}, ) featureCode: Optional[str] = Field( default=None, description="Feature code of the owning instance", json_schema_extra={"label": "Feature", "frontend_type": "text", "frontend_readonly": True}, ) isRunning: Optional[bool] = Field( default=None, description="Whether the workflow currently has an active run", json_schema_extra={ "label": "Läuft", "frontend_type": "checkbox", "frontend_readonly": True, "frontend_format_labels": ["Ja", "-", "Nein"], }, )