gateway/modules/datamodels/datamodelViews.py
2026-04-26 22:53:44 +02:00

311 lines
13 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.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"],
},
)