chore: sync GovOPlaN module split state
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
# GovOPlaN Tenancy
|
||||
|
||||
`govoplan-tenancy` owns the live `tenants` table, tenant lifecycle, and tenant
|
||||
settings API route contributions during the GovOPlaN module split.
|
||||
`govoplan-tenancy` owns the live `tenancy_tenants` table, tenant lifecycle, and
|
||||
tenant settings API route contributions during the GovOPlaN module split.
|
||||
|
||||
`govoplan-access` depends on this module, and core's registry inserts tenancy
|
||||
before access for authenticated platform composition. The historical `tenants`
|
||||
table name is intentionally preserved for migration compatibility.
|
||||
before access for authenticated platform composition. Core migrations rename the
|
||||
historical `tenants` table to the module-prefixed table name for existing
|
||||
development databases.
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: govoplan-tenancy
|
||||
Version: 0.1.4
|
||||
Version: 0.1.6
|
||||
Summary: GovOPlaN tenancy platform module.
|
||||
Author: GovOPlaN
|
||||
Requires-Python: >=3.12
|
||||
Description-Content-Type: text/markdown
|
||||
Requires-Dist: govoplan-core>=0.1.4
|
||||
Requires-Dist: govoplan-core>=0.1.6
|
||||
|
||||
# GovOPlaN Tenancy
|
||||
|
||||
`govoplan-tenancy` owns the live `tenants` table, tenant lifecycle, and tenant
|
||||
settings API route contributions during the GovOPlaN module split.
|
||||
`govoplan-tenancy` owns the live `tenancy_tenants` table, tenant lifecycle, and
|
||||
tenant settings API route contributions during the GovOPlaN module split.
|
||||
|
||||
`govoplan-access` depends on this module, and core's registry inserts tenancy
|
||||
before access for authenticated platform composition. The historical `tenants`
|
||||
table name is intentionally preserved for migration compatibility.
|
||||
before access for authenticated platform composition. Core migrations rename the
|
||||
historical `tenants` table to the module-prefixed table name for existing
|
||||
development databases.
|
||||
|
||||
@@ -1 +1 @@
|
||||
govoplan-core>=0.1.4
|
||||
govoplan-core>=0.1.6
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,15 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import and_, func, or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.admin.common import AdminValidationError, slugify
|
||||
from govoplan_core.admin.settings import get_system_settings
|
||||
from govoplan_access.backend.auth.dependencies import ApiPrincipal, get_api_principal, has_scope, require_scope
|
||||
from govoplan_access.auth import ApiPrincipal, get_api_principal, has_scope, require_scope
|
||||
from govoplan_core.audit.logging import audit_event
|
||||
from govoplan_core.core.access import CAPABILITY_ACCESS_TENANT_PROVISIONER, TenantAccessProvisioner
|
||||
from govoplan_core.core.change_sequence import ChangeSequenceEntry, decode_sequence_watermark, encode_sequence_watermark, record_change, sequence_watermark_is_expired
|
||||
from govoplan_core.core.runtime import get_registry
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_core.i18n import (
|
||||
i18n_settings,
|
||||
normalize_enabled_language_codes,
|
||||
system_enabled_language_codes,
|
||||
system_i18n_payload,
|
||||
tenant_enabled_language_codes,
|
||||
update_i18n_settings,
|
||||
)
|
||||
from govoplan_core.tenancy.service import (
|
||||
assert_tenant_governance_override_allowed,
|
||||
effective_tenant_governance,
|
||||
@@ -20,16 +30,27 @@ from govoplan_tenancy.backend.db.models import Tenant
|
||||
from .schemas import (
|
||||
TenantAdminItem,
|
||||
TenantCreateRequest,
|
||||
TenantListDeltaResponse,
|
||||
TenantListResponse,
|
||||
TenantOwnerCandidate,
|
||||
TenantOwnerCandidateListResponse,
|
||||
TenantSettingsItem,
|
||||
TenantSettingsDeltaResponse,
|
||||
TenantSettingsUpdateRequest,
|
||||
TenantUpdateRequest,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||
|
||||
TENANCY_MODULE_ID = "tenancy"
|
||||
TENANT_LIST_COLLECTION = "tenancy.tenants"
|
||||
TENANT_LIST_RESOURCE = "tenant"
|
||||
TENANT_SETTINGS_COLLECTION = "tenancy.tenant_settings"
|
||||
TENANT_SETTINGS_RESOURCE = "tenant_settings_section"
|
||||
ADMIN_MODULE_ID = "admin"
|
||||
ADMIN_SYSTEM_SETTINGS_COLLECTION = "admin.system_settings"
|
||||
TENANT_SETTINGS_SECTIONS = ("identity", "locale", "languages", "settings")
|
||||
|
||||
|
||||
def _tenant_access_provisioner() -> TenantAccessProvisioner:
|
||||
registry = get_registry()
|
||||
@@ -70,16 +91,169 @@ def _tenant_item(session: Session, tenant: Tenant) -> TenantAdminItem:
|
||||
)
|
||||
|
||||
|
||||
def _tenant_settings_item(tenant: Tenant) -> TenantSettingsItem:
|
||||
def _tenant_settings_item(session: Session, tenant: Tenant) -> TenantSettingsItem:
|
||||
system_settings = get_system_settings(session)
|
||||
system_payload = system_i18n_payload(system_settings)
|
||||
system_enabled = system_enabled_language_codes(system_settings.settings, default_locale=system_settings.default_locale)
|
||||
enabled = tenant_enabled_language_codes(tenant.settings, system_enabled, default_locale=tenant.default_locale)
|
||||
return TenantSettingsItem(
|
||||
id=tenant.id,
|
||||
slug=tenant.slug,
|
||||
name=tenant.name,
|
||||
default_locale=tenant.default_locale,
|
||||
available_languages=system_payload["available_languages"],
|
||||
system_enabled_language_codes=system_enabled,
|
||||
enabled_language_codes=enabled,
|
||||
settings=tenant.settings or {},
|
||||
)
|
||||
|
||||
|
||||
def _tenant_settings_sections(item: TenantSettingsItem) -> dict[str, Any]:
|
||||
payload = item.model_dump(mode="json")
|
||||
return {
|
||||
"identity": {"id": payload["id"], "slug": payload["slug"], "name": payload["name"]},
|
||||
"locale": {"default_locale": payload["default_locale"]},
|
||||
"languages": {
|
||||
"available_languages": payload["available_languages"],
|
||||
"system_enabled_language_codes": payload["system_enabled_language_codes"],
|
||||
"enabled_language_codes": payload["enabled_language_codes"],
|
||||
},
|
||||
"settings": payload["settings"],
|
||||
}
|
||||
|
||||
|
||||
def _record_tenant_settings_section_changes(
|
||||
session: Session,
|
||||
*,
|
||||
tenant_id: str,
|
||||
before: dict[str, Any],
|
||||
after: dict[str, Any],
|
||||
principal: ApiPrincipal,
|
||||
) -> None:
|
||||
for section in TENANT_SETTINGS_SECTIONS:
|
||||
if before.get(section) == after.get(section):
|
||||
continue
|
||||
record_change(
|
||||
session,
|
||||
module_id=TENANCY_MODULE_ID,
|
||||
collection=TENANT_SETTINGS_COLLECTION,
|
||||
resource_type=TENANT_SETTINGS_RESOURCE,
|
||||
resource_id=section,
|
||||
operation="updated",
|
||||
tenant_id=tenant_id,
|
||||
actor_type="user",
|
||||
actor_id=principal.user.id,
|
||||
payload={"section": section},
|
||||
)
|
||||
|
||||
|
||||
def _record_tenant_list_change(session: Session, *, tenant: Tenant, operation: str, principal: ApiPrincipal) -> None:
|
||||
record_change(
|
||||
session,
|
||||
module_id=TENANCY_MODULE_ID,
|
||||
collection=TENANT_LIST_COLLECTION,
|
||||
resource_type=TENANT_LIST_RESOURCE,
|
||||
resource_id=tenant.id,
|
||||
operation=operation,
|
||||
tenant_id=None,
|
||||
actor_type="user",
|
||||
actor_id=principal.user.id,
|
||||
payload={"slug": tenant.slug, "name": tenant.name, "is_active": tenant.is_active},
|
||||
)
|
||||
|
||||
|
||||
def _tenant_list_delta_query(session: Session, *, since_sequence: int):
|
||||
return session.query(ChangeSequenceEntry).filter(
|
||||
ChangeSequenceEntry.id > since_sequence,
|
||||
ChangeSequenceEntry.module_id == TENANCY_MODULE_ID,
|
||||
ChangeSequenceEntry.collection == TENANT_LIST_COLLECTION,
|
||||
)
|
||||
|
||||
|
||||
def _tenant_list_watermark(session: Session) -> str:
|
||||
sequence_id = _tenant_list_delta_query(session, since_sequence=0).with_entities(func.max(ChangeSequenceEntry.id)).scalar()
|
||||
return encode_sequence_watermark(int(sequence_id or 0))
|
||||
|
||||
|
||||
def _tenant_list_delta_entries(session: Session, *, since: str, limit: int):
|
||||
try:
|
||||
since_sequence = decode_sequence_watermark(since)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||
if sequence_watermark_is_expired(
|
||||
session,
|
||||
since=since_sequence,
|
||||
tenant_id=None,
|
||||
module_id=TENANCY_MODULE_ID,
|
||||
collections=(TENANT_LIST_COLLECTION,),
|
||||
):
|
||||
return None, False
|
||||
entries_plus_one = _tenant_list_delta_query(session, since_sequence=since_sequence).order_by(ChangeSequenceEntry.id.asc()).limit(limit + 1).all()
|
||||
has_more = len(entries_plus_one) > limit
|
||||
return entries_plus_one[:limit], has_more
|
||||
|
||||
|
||||
def _tenant_list_response_watermark(session: Session, *, entries, has_more: bool) -> str:
|
||||
return encode_sequence_watermark(entries[-1].id) if has_more and entries else _tenant_list_watermark(session)
|
||||
|
||||
|
||||
def _tenant_list_deleted_entries(entries: list[ChangeSequenceEntry], visible_tenant_ids: set[str]):
|
||||
return [
|
||||
{"id": entry.resource_id, "resource_type": entry.resource_type or TENANT_LIST_RESOURCE}
|
||||
for entry in entries
|
||||
if entry.resource_id and (entry.operation == "deleted" or entry.resource_id not in visible_tenant_ids)
|
||||
]
|
||||
|
||||
|
||||
def _tenant_settings_delta_query(session: Session, *, tenant_id: str, since_sequence: int):
|
||||
return session.query(ChangeSequenceEntry).filter(
|
||||
ChangeSequenceEntry.id > since_sequence,
|
||||
or_(
|
||||
and_(
|
||||
ChangeSequenceEntry.module_id == TENANCY_MODULE_ID,
|
||||
ChangeSequenceEntry.collection == TENANT_SETTINGS_COLLECTION,
|
||||
ChangeSequenceEntry.tenant_id == tenant_id,
|
||||
),
|
||||
and_(
|
||||
ChangeSequenceEntry.module_id == ADMIN_MODULE_ID,
|
||||
ChangeSequenceEntry.collection == ADMIN_SYSTEM_SETTINGS_COLLECTION,
|
||||
ChangeSequenceEntry.resource_id == "languages",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _tenant_settings_watermark(session: Session, *, tenant_id: str) -> str:
|
||||
sequence_id = _tenant_settings_delta_query(session, tenant_id=tenant_id, since_sequence=0).with_entities(func.max(ChangeSequenceEntry.id)).scalar()
|
||||
return encode_sequence_watermark(int(sequence_id or 0))
|
||||
|
||||
|
||||
def _tenant_settings_delta_entries(session: Session, *, tenant_id: str, since: str, limit: int):
|
||||
try:
|
||||
since_sequence = decode_sequence_watermark(since)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||
if sequence_watermark_is_expired(
|
||||
session,
|
||||
since=since_sequence,
|
||||
tenant_id=tenant_id,
|
||||
module_id=TENANCY_MODULE_ID,
|
||||
collections=(TENANT_SETTINGS_COLLECTION,),
|
||||
):
|
||||
return None, False
|
||||
entries_plus_one = _tenant_settings_delta_query(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
since_sequence=since_sequence,
|
||||
).order_by(ChangeSequenceEntry.id.asc()).limit(limit + 1).all()
|
||||
has_more = len(entries_plus_one) > limit
|
||||
return entries_plus_one[:limit], has_more
|
||||
|
||||
|
||||
def _tenant_settings_response_watermark(session: Session, *, tenant_id: str, entries, has_more: bool) -> str:
|
||||
return encode_sequence_watermark(entries[-1].id) if has_more and entries else _tenant_settings_watermark(session, tenant_id=tenant_id)
|
||||
|
||||
|
||||
@router.get("/tenants", response_model=TenantListResponse)
|
||||
def list_tenants(
|
||||
session: Session = Depends(get_session),
|
||||
@@ -89,6 +263,44 @@ def list_tenants(
|
||||
return TenantListResponse(tenants=[_tenant_item(session, tenant) for tenant in tenants])
|
||||
|
||||
|
||||
def _full_tenant_list_delta_response(session: Session) -> TenantListDeltaResponse:
|
||||
tenants = session.query(Tenant).order_by(Tenant.name.asc()).all()
|
||||
return TenantListDeltaResponse(
|
||||
tenants=[_tenant_item(session, tenant) for tenant in tenants],
|
||||
deleted=[],
|
||||
watermark=_tenant_list_watermark(session),
|
||||
has_more=False,
|
||||
full=True,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tenants/delta", response_model=TenantListDeltaResponse)
|
||||
def list_tenants_delta(
|
||||
since: str | None = None,
|
||||
limit: int = Query(default=100, ge=1, le=500),
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_scope("system:tenants:read")),
|
||||
):
|
||||
del principal
|
||||
if since is None:
|
||||
return _full_tenant_list_delta_response(session)
|
||||
entries, has_more = _tenant_list_delta_entries(session, since=since, limit=limit)
|
||||
if entries is None:
|
||||
return _full_tenant_list_delta_response(session)
|
||||
changed_ids = [entry.resource_id for entry in entries if entry.resource_id and entry.operation != "deleted"]
|
||||
tenants = []
|
||||
if changed_ids:
|
||||
tenants = session.query(Tenant).filter(Tenant.id.in_(changed_ids)).order_by(Tenant.name.asc()).all()
|
||||
visible_tenant_ids = {tenant.id for tenant in tenants}
|
||||
return TenantListDeltaResponse(
|
||||
tenants=[_tenant_item(session, tenant) for tenant in tenants],
|
||||
deleted=_tenant_list_deleted_entries(entries, visible_tenant_ids),
|
||||
watermark=_tenant_list_response_watermark(session, entries=entries, has_more=has_more),
|
||||
has_more=has_more,
|
||||
full=False,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tenants/owner-candidates", response_model=TenantOwnerCandidateListResponse)
|
||||
def list_tenant_owner_candidates(
|
||||
session: Session = Depends(get_session),
|
||||
@@ -119,7 +331,7 @@ def create_tenant(
|
||||
assert_tenant_governance_override_allowed(session, field="allow_custom_roles", value=payload.allow_custom_roles)
|
||||
assert_tenant_governance_override_allowed(session, field="allow_api_keys", value=payload.allow_api_keys)
|
||||
except AdminValidationError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
|
||||
tenant = Tenant(
|
||||
slug=tenant_slug,
|
||||
name=payload.name.strip(),
|
||||
@@ -141,7 +353,7 @@ def create_tenant(
|
||||
owner_account_id=owner_account_id,
|
||||
)
|
||||
except AdminValidationError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
|
||||
audit_event(
|
||||
session,
|
||||
tenant_id=tenant.id,
|
||||
@@ -152,6 +364,7 @@ def create_tenant(
|
||||
scope="system",
|
||||
details={"slug": tenant.slug, "name": tenant.name, "creator_account_id": principal.account_id, "owner_account_id": owner_account_id},
|
||||
)
|
||||
_record_tenant_list_change(session, tenant=tenant, operation="created", principal=principal)
|
||||
session.commit()
|
||||
return _tenant_item(session, tenant)
|
||||
|
||||
@@ -171,6 +384,7 @@ def update_tenant(
|
||||
tenant = session.get(Tenant, tenant_id)
|
||||
if tenant is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tenant not found")
|
||||
before_sections = _tenant_settings_sections(_tenant_settings_item(session, tenant))
|
||||
if payload.name is not None:
|
||||
tenant.name = payload.name.strip()
|
||||
if "description" in payload.model_fields_set:
|
||||
@@ -190,7 +404,7 @@ def update_tenant(
|
||||
assert_tenant_governance_override_allowed(session, field="allow_api_keys", value=payload.allow_api_keys)
|
||||
tenant.allow_api_keys = payload.allow_api_keys
|
||||
except AdminValidationError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
|
||||
if payload.is_active is not None:
|
||||
if not payload.is_active and tenant.id == principal.tenant_id:
|
||||
raise HTTPException(
|
||||
@@ -209,6 +423,9 @@ def update_tenant(
|
||||
object_id=tenant.id,
|
||||
details=payload.model_dump(exclude_unset=True),
|
||||
)
|
||||
after_sections = _tenant_settings_sections(_tenant_settings_item(session, tenant))
|
||||
_record_tenant_settings_section_changes(session, tenant_id=tenant.id, before=before_sections, after=after_sections, principal=principal)
|
||||
_record_tenant_list_change(session, tenant=tenant, operation="updated", principal=principal)
|
||||
session.commit()
|
||||
return _tenant_item(session, tenant)
|
||||
|
||||
@@ -221,7 +438,55 @@ def get_tenant_settings(
|
||||
tenant = session.get(Tenant, principal.tenant_id)
|
||||
if tenant is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tenant not found")
|
||||
return _tenant_settings_item(tenant)
|
||||
return _tenant_settings_item(session, tenant)
|
||||
|
||||
|
||||
def _full_tenant_settings_delta_response(session: Session, tenant: Tenant) -> TenantSettingsDeltaResponse:
|
||||
item = _tenant_settings_item(session, tenant)
|
||||
return TenantSettingsDeltaResponse(
|
||||
item=item,
|
||||
sections=_tenant_settings_sections(item),
|
||||
changed_sections=list(TENANT_SETTINGS_SECTIONS),
|
||||
deleted=[],
|
||||
watermark=_tenant_settings_watermark(session, tenant_id=tenant.id),
|
||||
has_more=False,
|
||||
full=True,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tenant/settings/delta", response_model=TenantSettingsDeltaResponse)
|
||||
def get_tenant_settings_delta(
|
||||
since: str | None = None,
|
||||
limit: int = Query(default=100, ge=1, le=500),
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_scope("admin:settings:read")),
|
||||
):
|
||||
tenant = session.get(Tenant, principal.tenant_id)
|
||||
if tenant is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tenant not found")
|
||||
if since is None:
|
||||
return _full_tenant_settings_delta_response(session, tenant)
|
||||
entries, has_more = _tenant_settings_delta_entries(session, tenant_id=tenant.id, since=since, limit=limit)
|
||||
if entries is None:
|
||||
return _full_tenant_settings_delta_response(session, tenant)
|
||||
changed = set()
|
||||
for entry in entries:
|
||||
if entry.module_id == ADMIN_MODULE_ID and entry.collection == ADMIN_SYSTEM_SETTINGS_COLLECTION and entry.resource_id == "languages":
|
||||
changed.add("languages")
|
||||
elif entry.resource_type == TENANT_SETTINGS_RESOURCE:
|
||||
changed.add(entry.resource_id)
|
||||
changed_sections = [section for section in TENANT_SETTINGS_SECTIONS if section in changed]
|
||||
item = _tenant_settings_item(session, tenant)
|
||||
sections = _tenant_settings_sections(item)
|
||||
return TenantSettingsDeltaResponse(
|
||||
item=None,
|
||||
sections={section: sections[section] for section in changed_sections},
|
||||
changed_sections=changed_sections,
|
||||
deleted=[],
|
||||
watermark=_tenant_settings_response_watermark(session, tenant_id=tenant.id, entries=entries, has_more=has_more),
|
||||
has_more=has_more,
|
||||
full=False,
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/tenant/settings", response_model=TenantSettingsItem)
|
||||
@@ -233,7 +498,19 @@ def update_tenant_settings(
|
||||
tenant = session.get(Tenant, principal.tenant_id)
|
||||
if tenant is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tenant not found")
|
||||
tenant.default_locale = payload.default_locale.strip() or "en"
|
||||
before_sections = _tenant_settings_sections(_tenant_settings_item(session, tenant))
|
||||
system_settings = get_system_settings(session)
|
||||
system_enabled = system_enabled_language_codes(system_settings.settings, default_locale=system_settings.default_locale)
|
||||
current_i18n = i18n_settings(tenant.settings)
|
||||
raw_enabled = payload.enabled_language_codes if "enabled_language_codes" in payload.model_fields_set else current_i18n.get("enabled_language_codes")
|
||||
enabled = normalize_enabled_language_codes(
|
||||
raw_enabled,
|
||||
[{"code": code} for code in system_enabled],
|
||||
default_locale=payload.default_locale,
|
||||
fallback_codes=system_enabled,
|
||||
)
|
||||
tenant.default_locale = payload.default_locale.strip() or enabled[0]
|
||||
tenant.settings = update_i18n_settings(tenant.settings, enabled_language_codes=enabled)
|
||||
session.add(tenant)
|
||||
audit_event(
|
||||
session,
|
||||
@@ -242,7 +519,9 @@ def update_tenant_settings(
|
||||
action="tenant.settings.updated",
|
||||
object_type="tenant",
|
||||
object_id=tenant.id,
|
||||
details={"default_locale": tenant.default_locale},
|
||||
details={"default_locale": tenant.default_locale, "enabled_language_codes": enabled},
|
||||
)
|
||||
after_sections = _tenant_settings_sections(_tenant_settings_item(session, tenant))
|
||||
_record_tenant_settings_section_changes(session, tenant_id=tenant.id, before=before_sections, after=after_sections, principal=principal)
|
||||
session.commit()
|
||||
return _tenant_settings_item(tenant)
|
||||
return _tenant_settings_item(session, tenant)
|
||||
|
||||
@@ -5,6 +5,8 @@ from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
from govoplan_core.api.v1.schemas import DeltaDeletedItem
|
||||
|
||||
|
||||
class TenantAdminItem(BaseModel):
|
||||
id: str
|
||||
@@ -27,6 +29,14 @@ class TenantListResponse(BaseModel):
|
||||
tenants: list[TenantAdminItem]
|
||||
|
||||
|
||||
class TenantListDeltaResponse(BaseModel):
|
||||
tenants: list[TenantAdminItem] = Field(default_factory=list)
|
||||
deleted: list[DeltaDeletedItem] = Field(default_factory=list)
|
||||
watermark: str | None = None
|
||||
has_more: bool = False
|
||||
full: bool = False
|
||||
|
||||
|
||||
class TenantOwnerCandidate(BaseModel):
|
||||
account_id: str
|
||||
email: str
|
||||
@@ -69,10 +79,24 @@ class TenantSettingsItem(BaseModel):
|
||||
slug: str
|
||||
name: str
|
||||
default_locale: str = Field(default="en", min_length=1, max_length=20)
|
||||
available_languages: list[dict[str, Any]] = Field(default_factory=list)
|
||||
system_enabled_language_codes: list[str] = Field(default_factory=list)
|
||||
enabled_language_codes: list[str] = Field(default_factory=list)
|
||||
settings: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class TenantSettingsDeltaResponse(BaseModel):
|
||||
item: TenantSettingsItem | None = None
|
||||
sections: dict[str, Any] = Field(default_factory=dict)
|
||||
changed_sections: list[str] = Field(default_factory=list)
|
||||
deleted: list[DeltaDeletedItem] = Field(default_factory=list)
|
||||
watermark: str | None = None
|
||||
has_more: bool = False
|
||||
full: bool = False
|
||||
|
||||
|
||||
class TenantSettingsUpdateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
default_locale: str = Field(min_length=1, max_length=20)
|
||||
enabled_language_codes: list[str] | None = None
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -14,7 +14,7 @@ def new_uuid() -> str:
|
||||
|
||||
|
||||
class Tenant(Base, TimestampMixin):
|
||||
__tablename__ = "tenants"
|
||||
__tablename__ = "tenancy_tenants"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
slug: Mapped[str] = mapped_column(String(100), unique=True, nullable=False, index=True)
|
||||
|
||||
Reference in New Issue
Block a user