gateway/modules/datamodels/datamodelViews.py
2026-04-26 18:11:42 +02:00

199 lines
8.5 KiB
Python

# 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.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]