from __future__ import annotations import hashlib import json import re from collections.abc import Iterable from dataclasses import dataclass from datetime import datetime, timezone from typing import Literal from sqlalchemy import or_ from sqlalchemy.orm import Session from govoplan_core.core.views import ( VIEW_SURFACE_CONTRACT_VERSION, EffectiveView, ViewSurface, ) from govoplan_core.db.base import utcnow from govoplan_views.backend.db.models import ( ViewAssignment, ViewDefinition, ViewPreference, ViewRevision, ) ASSIGNMENT_MODES = frozenset({"available", "default", "required"}) ASSIGNMENT_SCOPES = frozenset({"system", "tenant", "group", "user"}) DEFINITION_SCOPES = frozenset({"system", "tenant", "group", "user"}) LOCKOUT_BASE_SURFACE_IDS = ( "access.module", "access.nav.admin", "access.route.admin", "views.module", "views.selector", ) LOCKOUT_ADMIN_SURFACE_IDS = { "system": "views.admin.system", "tenant": "views.admin.tenant", "group": "views.admin.tenant", "user": "views.admin.tenant", } _KEY_RE = re.compile(r"[^a-z0-9]+") class ViewsError(RuntimeError): pass class ViewsNotFoundError(ViewsError): pass class ViewsConflictError(ViewsError): pass class ViewsValidationError(ViewsError): pass @dataclass(frozen=True, slots=True) class EffectiveViewOption: id: str name: str description: str | None revision_id: str @dataclass(frozen=True, slots=True) class ViewDiagnostic: severity: Literal["warning", "error"] code: str message: str surface_ids: tuple[str, ...] = () @dataclass(frozen=True, slots=True) class EffectiveViewState: effective: EffectiveView available_views: tuple[EffectiveViewOption, ...] diagnostics: tuple[ViewDiagnostic, ...] = () def _now() -> datetime: return utcnow() def _scope_values( scope_type: str, *, tenant_id: str, scope_id: str | None = None, ) -> tuple[str | None, str | None, str]: if scope_type == "system": if scope_id: raise ViewsValidationError( "System View definitions cannot declare a target id" ) return None, None, "system" if scope_type == "tenant": if scope_id and scope_id != tenant_id: raise ViewsValidationError( "Tenant View definitions must target the active tenant" ) return tenant_id, tenant_id, f"tenant:{tenant_id}" if scope_type in {"group", "user"}: target_id = (scope_id or "").strip() if not target_id: raise ViewsValidationError( f"{scope_type.title()} View definitions require a target id" ) return tenant_id, target_id, f"{scope_type}:{tenant_id}:{target_id}" raise ViewsValidationError(f"Unsupported View definition scope: {scope_type}") def _assignment_target( scope_type: str, scope_id: str | None, *, tenant_id: str, ) -> tuple[str | None, str | None, str]: normalized_scope = scope_type.strip().lower() if normalized_scope not in ASSIGNMENT_SCOPES: raise ViewsValidationError(f"Unsupported View assignment scope: {scope_type}") if normalized_scope == "system": if scope_id: raise ViewsValidationError( "System View assignments cannot declare a target id" ) return None, None, "system" if normalized_scope == "tenant": if scope_id and scope_id != tenant_id: raise ViewsValidationError( "Tenant View assignments must target the active tenant" ) return tenant_id, tenant_id, f"tenant:{tenant_id}" target_id = (scope_id or "").strip() if not target_id: raise ViewsValidationError( f"{normalized_scope.title()} View assignments require a target id" ) return tenant_id, target_id, f"{normalized_scope}:{tenant_id}:{target_id}" def _definition_key(value: str | None, name: str) -> str: source = (value or name).strip().lower() normalized = _KEY_RE.sub("-", source).strip("-") if not normalized: raise ViewsValidationError("View key must contain letters or numbers") return normalized[:120] def _definition_query_for_actor( session: Session, *, tenant_id: str, include_system: bool, ): query = session.query(ViewDefinition).filter(ViewDefinition.deleted_at.is_(None)) if include_system: return query.filter( or_( ViewDefinition.tenant_id == tenant_id, ViewDefinition.tenant_id.is_(None), ) ) return query.filter(ViewDefinition.tenant_id == tenant_id) def list_definitions( session: Session, *, tenant_id: str, scope_type: str, scope_id: str | None = None, include_inherited: bool = True, ) -> list[ViewDefinition]: if scope_type not in DEFINITION_SCOPES: raise ViewsValidationError(f"Unsupported View definition scope: {scope_type}") if scope_type == "system": query = session.query(ViewDefinition).filter( ViewDefinition.tenant_id.is_(None), ViewDefinition.scope_type == "system", ViewDefinition.deleted_at.is_(None), ) elif scope_type == "tenant": if not include_inherited: query = session.query(ViewDefinition).filter( ViewDefinition.tenant_id == tenant_id, ViewDefinition.scope_type == "tenant", ViewDefinition.deleted_at.is_(None), ) else: query = _definition_query_for_actor( session, tenant_id=tenant_id, include_system=True, ) else: target_id = (scope_id or "").strip() if not target_id: raise ViewsValidationError( f"{scope_type.title()} View definition listings require a target id" ) exact_target = ( (ViewDefinition.tenant_id == tenant_id) & (ViewDefinition.scope_type == scope_type) & (ViewDefinition.scope_id == target_id) ) query = session.query(ViewDefinition).filter( ViewDefinition.deleted_at.is_(None) ) if include_inherited: query = query.filter( or_( exact_target, (ViewDefinition.tenant_id == tenant_id) & (ViewDefinition.scope_type == "tenant"), (ViewDefinition.tenant_id.is_(None)) & (ViewDefinition.scope_type == "system"), ) ) else: query = query.filter(exact_target) return query.order_by( ViewDefinition.scope_type.asc(), ViewDefinition.name.asc(), ViewDefinition.id.asc(), ).all() def get_definition( session: Session, *, tenant_id: str, definition_id: str, include_system: bool = True, ) -> ViewDefinition: definition = ( _definition_query_for_actor( session, tenant_id=tenant_id, include_system=include_system, ) .filter(ViewDefinition.id == definition_id) .first() ) if definition is None: raise ViewsNotFoundError("View definition not found") return definition def get_revision( session: Session, *, definition_id: str, revision_id: str | None = None, revision: int | None = None, ) -> ViewRevision: query = session.query(ViewRevision).filter( ViewRevision.definition_id == definition_id ) if revision_id is not None: query = query.filter(ViewRevision.id == revision_id) elif revision is not None: query = query.filter(ViewRevision.revision == revision) else: query = query.order_by(ViewRevision.revision.desc()) result = query.first() if result is None: raise ViewsNotFoundError("View revision not found") return result def definition_revisions( session: Session, *, definition_id: str, ) -> list[ViewRevision]: return ( session.query(ViewRevision) .filter(ViewRevision.definition_id == definition_id) .order_by(ViewRevision.revision.desc()) .all() ) def normalize_visible_surface_ids( values: Iterable[str], *, catalogue: Iterable[ViewSurface], ) -> list[str]: surfaces = tuple(catalogue) by_id = {surface.id: surface for surface in surfaces} selected = {str(value).strip() for value in values if str(value).strip()} unknown = sorted(selected - set(by_id)) if unknown: preview = ", ".join(unknown[:5]) suffix = "" if len(unknown) <= 5 else f" and {len(unknown) - 5} more" raise ViewsValidationError(f"Unknown View surfaces: {preview}{suffix}") selected.update(surface.id for surface in surfaces if surface.required) pending = list(selected) while pending: surface_id = pending.pop() surface = by_id[surface_id] if surface.parent_id and surface.parent_id not in selected: if surface.parent_id not in by_id: raise ViewsValidationError( f"View surface {surface.id} has unavailable parent {surface.parent_id}" ) selected.add(surface.parent_id) pending.append(surface.parent_id) selected_surfaces = [by_id[surface_id] for surface_id in selected] if not any(surface.kind == "navigation" for surface in selected_surfaces): raise ViewsValidationError("A View must keep at least one navigation entry") if not any(surface.kind == "route" for surface in selected_surfaces): raise ViewsValidationError("A View must keep at least one route") order = {surface.id: index for index, surface in enumerate(surfaces)} return sorted( selected, key=lambda surface_id: (order.get(surface_id, 10_000), surface_id) ) def _revision_hash(surface_ids: list[str]) -> str: payload = { "surface_contract_version": VIEW_SURFACE_CONTRACT_VERSION, "visible_surface_ids": surface_ids, } return hashlib.sha256( json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8") ).hexdigest() def create_definition( session: Session, *, tenant_id: str, scope_type: str, scope_id: str | None = None, definition_key: str | None, name: str, description: str | None, visible_surface_ids: Iterable[str], catalogue: Iterable[ViewSurface], actor_id: str | None, ) -> ViewDefinition: if scope_type not in DEFINITION_SCOPES: raise ViewsValidationError(f"Unsupported View definition scope: {scope_type}") row_tenant_id, scope_id, scope_key = _scope_values( scope_type, tenant_id=tenant_id, scope_id=scope_id, ) clean_name = name.strip() if not clean_name: raise ViewsValidationError("View name is required") clean_key = _definition_key(definition_key, clean_name) duplicate = ( session.query(ViewDefinition.id) .filter( ViewDefinition.scope_key == scope_key, ViewDefinition.definition_key == clean_key, ViewDefinition.deleted_at.is_(None), ) .first() ) if duplicate is not None: raise ViewsConflictError( f"A View with key {clean_key!r} already exists in this scope" ) normalized_surfaces = normalize_visible_surface_ids( visible_surface_ids, catalogue=catalogue, ) definition = ViewDefinition( tenant_id=row_tenant_id, scope_type=scope_type, scope_id=scope_id, scope_key=scope_key, definition_key=clean_key, name=clean_name, description=(description or "").strip() or None, status="draft", current_revision=1, created_by=actor_id, updated_by=actor_id, ) session.add(definition) session.flush() revision = ViewRevision( tenant_id=row_tenant_id, definition_id=definition.id, revision=1, surface_contract_version=VIEW_SURFACE_CONTRACT_VERSION, visible_surface_ids=normalized_surfaces, content_hash=_revision_hash(normalized_surfaces), created_by=actor_id, ) session.add(revision) session.flush() return definition def update_definition( session: Session, definition: ViewDefinition, *, name: str | None, description: str | None, actor_id: str | None, fields_set: set[str], ) -> ViewDefinition: if definition.status == "archived": raise ViewsConflictError("Archived Views cannot be edited") if "name" in fields_set: clean_name = (name or "").strip() if not clean_name: raise ViewsValidationError("View name is required") definition.name = clean_name if "description" in fields_set: definition.description = (description or "").strip() or None definition.updated_by = actor_id definition.updated_at = _now() session.flush() return definition def create_revision( session: Session, definition: ViewDefinition, *, visible_surface_ids: Iterable[str], catalogue: Iterable[ViewSurface], actor_id: str | None, ) -> ViewRevision: if definition.status == "archived": raise ViewsConflictError("Archived Views cannot be revised") normalized_surfaces = normalize_visible_surface_ids( visible_surface_ids, catalogue=catalogue, ) content_hash = _revision_hash(normalized_surfaces) latest = get_revision(session, definition_id=definition.id) if latest.content_hash == content_hash: return latest next_number = definition.current_revision + 1 revision = ViewRevision( tenant_id=definition.tenant_id, definition_id=definition.id, revision=next_number, surface_contract_version=VIEW_SURFACE_CONTRACT_VERSION, visible_surface_ids=normalized_surfaces, content_hash=content_hash, created_by=actor_id, ) session.add(revision) definition.current_revision = next_number definition.updated_by = actor_id definition.updated_at = _now() session.flush() return revision def lockout_required_surface_ids( catalogue: Iterable[ViewSurface], *, assignment_scope_type: str, ) -> frozenset[str]: known = {surface.id for surface in catalogue} required = {surface.id for surface in catalogue if surface.required} admin_surface = LOCKOUT_ADMIN_SURFACE_IDS.get(assignment_scope_type) escape_ids = set(LOCKOUT_BASE_SURFACE_IDS) if admin_surface: escape_ids.add(admin_surface) missing_contract = { "access.nav.admin", "access.route.admin", admin_surface, } - known missing_contract.discard(None) if missing_contract: raise ViewsValidationError( "Required View assignments are unavailable because the active module " "graph does not expose all administration escape surfaces: " + ", ".join(sorted(missing_contract)) ) return frozenset(required | (escape_ids & known)) def validate_required_assignment_revision( revision: ViewRevision, *, assignment_scope_type: str, catalogue: Iterable[ViewSurface], ) -> None: required_ids = lockout_required_surface_ids( catalogue, assignment_scope_type=assignment_scope_type, ) missing = sorted(required_ids - set(revision.visible_surface_ids)) if missing: raise ViewsValidationError( "A required View must retain its selector and administration escape " f"surfaces: {', '.join(missing)}" ) def publish_revision( session: Session, definition: ViewDefinition, revision: ViewRevision, *, catalogue: Iterable[ViewSurface], actor_id: str | None, ) -> ViewDefinition: if definition.status == "archived": raise ViewsConflictError("Archived Views cannot be published") tracking_required_assignments = ( session.query(ViewAssignment) .filter( ViewAssignment.definition_id == definition.id, ViewAssignment.mode == "required", ViewAssignment.is_active.is_(True), ViewAssignment.revision_id.is_(None), ) .all() ) for assignment in tracking_required_assignments: validate_required_assignment_revision( revision, assignment_scope_type=assignment.scope_type, catalogue=catalogue, ) definition.published_revision_id = revision.id definition.status = "published" definition.updated_by = actor_id definition.updated_at = _now() session.flush() ensure_owner_available_assignment( session, definition, catalogue=catalogue, actor_id=actor_id, ) return definition def ensure_owner_available_assignment( session: Session, definition: ViewDefinition, *, catalogue: Iterable[ViewSurface], actor_id: str | None, ) -> ViewAssignment | None: if definition.scope_type not in {"group", "user"}: return None if not definition.tenant_id or not definition.scope_id: raise ViewsValidationError( "Owned View definitions require tenant and target identifiers" ) target_key = f"{definition.scope_type}:{definition.tenant_id}:{definition.scope_id}" assignment = ( session.query(ViewAssignment) .filter( ViewAssignment.target_key == target_key, ViewAssignment.definition_id == definition.id, ViewAssignment.mode == "available", ) .first() ) if assignment is None: return create_assignment( session, tenant_id=definition.tenant_id, scope_type=definition.scope_type, scope_id=definition.scope_id, definition=definition, revision_id=None, mode="available", priority=0, is_active=True, metadata={"source": "definition_owner"}, catalogue=catalogue, actor_id=actor_id, ) assignment.revision_id = None assignment.is_active = True assignment.metadata_ = { **dict(assignment.metadata_ or {}), "source": "definition_owner", } assignment.updated_by = actor_id assignment.updated_at = _now() session.flush() return assignment def archive_definition( session: Session, definition: ViewDefinition, *, actor_id: str | None, ) -> ViewDefinition: required_count = ( session.query(ViewAssignment) .filter( ViewAssignment.definition_id == definition.id, ViewAssignment.mode == "required", ViewAssignment.is_active.is_(True), ) .count() ) if required_count: raise ViewsConflictError( "Remove or replace required assignments before archiving this View" ) session.query(ViewAssignment).filter( ViewAssignment.definition_id == definition.id ).update({"is_active": False}, synchronize_session=False) definition.status = "archived" definition.updated_by = actor_id definition.updated_at = _now() session.flush() return definition def list_assignments( session: Session, *, tenant_id: str, scope_type: str, include_inherited: bool = True, ) -> list[ViewAssignment]: if scope_type == "system": query = session.query(ViewAssignment).filter(ViewAssignment.tenant_id.is_(None)) elif scope_type == "tenant": query = session.query(ViewAssignment).filter( ViewAssignment.tenant_id == tenant_id ) if include_inherited: query = session.query(ViewAssignment).filter( or_( ViewAssignment.tenant_id == tenant_id, ViewAssignment.tenant_id.is_(None), ) ) else: raise ViewsValidationError( f"Unsupported assignment listing scope: {scope_type}" ) return query.order_by( ViewAssignment.scope_type.asc(), ViewAssignment.priority.desc(), ViewAssignment.created_at.asc(), ).all() def _assignment_revision( session: Session, assignment: ViewAssignment, ) -> ViewRevision | None: revision_id = assignment.revision_id or assignment.definition.published_revision_id if not revision_id: return None revision = session.get(ViewRevision, revision_id) if revision is None or revision.definition_id != assignment.definition_id: return None return revision def _validate_assignment_revision( session: Session, definition: ViewDefinition, revision_id: str | None, *, allow_historical_pin: bool = False, ) -> ViewRevision: if definition.status != "published" or not definition.published_revision_id: raise ViewsConflictError("Publish the View before assigning it") selected_revision_id = revision_id or definition.published_revision_id revision = session.get(ViewRevision, selected_revision_id) if revision is None or revision.definition_id != definition.id: raise ViewsValidationError("The selected revision does not belong to this View") if ( revision_id and revision_id != definition.published_revision_id and not allow_historical_pin ): raise ViewsValidationError( "Only the currently published revision can be pinned to a new assignment" ) return revision def _deactivate_competing_assignment( session: Session, *, assignment: ViewAssignment, ) -> None: if assignment.mode not in {"default", "required"} or not assignment.is_active: return ( session.query(ViewAssignment) .filter( ViewAssignment.target_key == assignment.target_key, ViewAssignment.mode == assignment.mode, ViewAssignment.is_active.is_(True), ViewAssignment.id != assignment.id, ) .update({"is_active": False}, synchronize_session=False) ) def create_assignment( session: Session, *, tenant_id: str, scope_type: str, scope_id: str | None, definition: ViewDefinition, revision_id: str | None, mode: str, priority: int, is_active: bool, metadata: dict[str, object], catalogue: Iterable[ViewSurface], actor_id: str | None, ) -> ViewAssignment: if mode not in ASSIGNMENT_MODES: raise ViewsValidationError(f"Unsupported View assignment mode: {mode}") row_tenant_id, target_id, target_key = _assignment_target( scope_type, scope_id, tenant_id=tenant_id, ) if scope_type == "system" and definition.scope_type != "system": raise ViewsValidationError("System assignments can only use system Views") if definition.tenant_id not in {None, tenant_id}: raise ViewsNotFoundError("View definition not found") revision = _validate_assignment_revision(session, definition, revision_id) if mode == "required" and is_active: validate_required_assignment_revision( revision, assignment_scope_type=scope_type, catalogue=catalogue, ) duplicate = ( session.query(ViewAssignment.id) .filter( ViewAssignment.target_key == target_key, ViewAssignment.definition_id == definition.id, ViewAssignment.mode == mode, ) .first() ) if duplicate is not None: raise ViewsConflictError( "This View already has an assignment with the same target and mode" ) assignment = ViewAssignment( tenant_id=row_tenant_id, scope_type=scope_type, scope_id=target_id, target_key=target_key, definition_id=definition.id, revision_id=revision_id, mode=mode, priority=priority, is_active=is_active, metadata_=metadata, created_by=actor_id, updated_by=actor_id, ) session.add(assignment) session.flush() _deactivate_competing_assignment(session, assignment=assignment) session.flush() return assignment def get_assignment( session: Session, *, tenant_id: str, assignment_id: str, include_system: bool = True, ) -> ViewAssignment: query = session.query(ViewAssignment).filter(ViewAssignment.id == assignment_id) if include_system: query = query.filter( or_( ViewAssignment.tenant_id == tenant_id, ViewAssignment.tenant_id.is_(None), ) ) else: query = query.filter(ViewAssignment.tenant_id == tenant_id) assignment = query.first() if assignment is None: raise ViewsNotFoundError("View assignment not found") return assignment def update_assignment( session: Session, assignment: ViewAssignment, *, updates: dict[str, object], catalogue: Iterable[ViewSurface], actor_id: str | None, ) -> ViewAssignment: next_mode = str(updates.get("mode", assignment.mode)) next_revision_id = ( updates["revision_id"] if "revision_id" in updates else assignment.revision_id ) next_active = bool(updates.get("is_active", assignment.is_active)) if next_mode not in ASSIGNMENT_MODES: raise ViewsValidationError(f"Unsupported View assignment mode: {next_mode}") revision = _validate_assignment_revision( session, assignment.definition, next_revision_id if isinstance(next_revision_id, str) else None, allow_historical_pin=( isinstance(next_revision_id, str) and next_revision_id == assignment.revision_id ), ) if next_mode == "required" and next_active: validate_required_assignment_revision( revision, assignment_scope_type=assignment.scope_type, catalogue=catalogue, ) duplicate = ( session.query(ViewAssignment.id) .filter( ViewAssignment.target_key == assignment.target_key, ViewAssignment.definition_id == assignment.definition_id, ViewAssignment.mode == next_mode, ViewAssignment.id != assignment.id, ) .first() ) if duplicate is not None: raise ViewsConflictError( "This View already has an assignment with the same target and mode" ) assignment.mode = next_mode if "revision_id" in updates: assignment.revision_id = ( str(next_revision_id) if next_revision_id is not None else None ) if "priority" in updates: assignment.priority = int(updates["priority"]) if "is_active" in updates: assignment.is_active = next_active if "metadata" in updates: assignment.metadata_ = dict(updates["metadata"] or {}) assignment.updated_by = actor_id assignment.updated_at = _now() session.flush() _deactivate_competing_assignment(session, assignment=assignment) session.flush() return assignment def delete_assignment(session: Session, assignment: ViewAssignment) -> None: session.delete(assignment) session.flush() def _assignment_applies( assignment: ViewAssignment, *, tenant_id: str, account_id: str, group_ids: frozenset[str], ) -> bool: if assignment.scope_type == "system": return True if assignment.tenant_id != tenant_id: return False if assignment.scope_type == "tenant": return assignment.scope_id == tenant_id if assignment.scope_type == "group": return bool(assignment.scope_id and assignment.scope_id in group_ids) if assignment.scope_type == "user": return assignment.scope_id == account_id return False def _assignment_order(assignment: ViewAssignment) -> tuple[int, int, float, str]: specificity = { "system": 0, "tenant": 1, "group": 2, "user": 3, }.get(assignment.scope_type, -1) updated_at = assignment.updated_at if updated_at.tzinfo is None: updated_at = updated_at.replace(tzinfo=timezone.utc) return specificity, assignment.priority, updated_at.timestamp(), assignment.id def _preference( session: Session, *, tenant_id: str, account_id: str, ) -> ViewPreference | None: return ( session.query(ViewPreference) .filter( ViewPreference.tenant_id == tenant_id, ViewPreference.account_id == account_id, ) .first() ) def resolve_effective_view( session: Session, *, tenant_id: str, account_id: str, group_ids: Iterable[str] = (), catalogue: Iterable[ViewSurface] | None = None, ) -> EffectiveViewState: groups = frozenset(str(group_id) for group_id in group_ids) candidates = ( session.query(ViewAssignment) .join(ViewDefinition, ViewAssignment.definition_id == ViewDefinition.id) .filter( ViewAssignment.is_active.is_(True), ViewDefinition.status == "published", ViewDefinition.deleted_at.is_(None), or_( ViewAssignment.tenant_id == tenant_id, ViewAssignment.tenant_id.is_(None), ), ) .all() ) applicable = [ assignment for assignment in candidates if _assignment_applies( assignment, tenant_id=tenant_id, account_id=account_id, group_ids=groups, ) ] resolved_assignments: list[tuple[ViewAssignment, ViewRevision]] = [] for assignment in applicable: revision = _assignment_revision(session, assignment) if revision is None: continue resolved_assignments.append((assignment, revision)) option_assignments: dict[str, tuple[ViewAssignment, ViewRevision]] = {} for assignment, revision in resolved_assignments: existing = option_assignments.get(assignment.definition_id) if existing is None or _assignment_order(assignment) > _assignment_order( existing[0] ): option_assignments[assignment.definition_id] = (assignment, revision) ordered_options = sorted( option_assignments.values(), key=lambda item: ( -_assignment_order(item[0])[0], -_assignment_order(item[0])[1], item[0].definition.name.lower(), item[0].definition_id, ), ) available_views = tuple( EffectiveViewOption( id=assignment.definition.id, name=assignment.definition.name, description=assignment.definition.description, revision_id=revision.id, ) for assignment, revision in ordered_options ) selected: tuple[ViewAssignment, ViewRevision] | None = None locked = False required = [pair for pair in resolved_assignments if pair[0].mode == "required"] preference = _preference( session, tenant_id=tenant_id, account_id=account_id, ) provenance: list[dict[str, object]] = [] if required: selected = max(required, key=lambda pair: _assignment_order(pair[0])) locked = True provenance.append( { "source": "required_assignment", "scope_type": selected[0].scope_type, "scope_id": selected[0].scope_id, "detail": f"Required View assignment {selected[0].id}", } ) elif preference and preference.selection_kind == "none": provenance.append( { "source": "user_selection", "scope_type": "user", "scope_id": account_id, "detail": "Full interface selected", } ) elif ( preference and preference.selection_kind == "view" and preference.view_id in option_assignments ): selected = option_assignments[preference.view_id] provenance.append( { "source": "user_selection", "scope_type": "user", "scope_id": account_id, "detail": f"Selected View {preference.view_id}", } ) else: defaults = [pair for pair in resolved_assignments if pair[0].mode == "default"] if defaults: selected = max(defaults, key=lambda pair: _assignment_order(pair[0])) provenance.append( { "source": "default_assignment", "scope_type": selected[0].scope_type, "scope_id": selected[0].scope_id, "detail": f"Default View assignment {selected[0].id}", } ) diagnostics: list[ViewDiagnostic] = [] if selected is not None and catalogue is not None: assignment, revision = selected current_catalogue = tuple(catalogue) known_surfaces = {surface.id: surface for surface in current_catalogue} stale_ids = sorted(set(revision.visible_surface_ids) - set(known_surfaces)) if stale_ids: diagnostics.append( ViewDiagnostic( severity="warning", code="view.stale_surfaces", message=( "The active View references surfaces that are no longer " "announced by the active module graph." ), surface_ids=tuple(stale_ids), ) ) if locked: try: lockout_ids = lockout_required_surface_ids( current_catalogue, assignment_scope_type=assignment.scope_type, ) unavailable_escape_ids = sorted( lockout_ids - set(revision.visible_surface_ids) ) lockout_message = ( "The required View no longer retains all administration " "escape surfaces." if unavailable_escape_ids else "" ) except ViewsValidationError as exc: unavailable_escape_ids = () lockout_message = str(exc) if lockout_message: diagnostics.append( ViewDiagnostic( severity="error", code="view.lockout_escape_unavailable", message=( f"{lockout_message} The full authorized interface " "is shown until the assignment is repaired." ), surface_ids=tuple(unavailable_escape_ids), ) ) provenance.append( { "source": "invalid_view_fallback", "scope_type": assignment.scope_type, "scope_id": assignment.scope_id, "detail": ( "Required View administration escape is unavailable " f"for revision {revision.id}" ), } ) selected = None locked = False known_visible = [ known_surfaces[surface_id] for surface_id in revision.visible_surface_ids if surface_id in known_surfaces ] if selected is not None and ( not any(surface.kind == "navigation" for surface in known_visible) or not any(surface.kind == "route" for surface in known_visible) ): diagnostics.append( ViewDiagnostic( severity="error", code="view.no_reachable_surface", message=( "The active View no longer contains a reachable navigation " "entry and route. The full authorized interface is shown." ), ) ) provenance.append( { "source": "invalid_view_fallback", "scope_type": assignment.scope_type, "scope_id": assignment.scope_id, "detail": f"Invalid View revision {revision.id}", } ) selected = None locked = False if selected is None: effective = EffectiveView( view_id=None, revision_id=None, name=None, visible_surface_ids=frozenset(), locked=False, provenance=tuple(provenance), ) else: assignment, revision = selected effective = EffectiveView( view_id=assignment.definition.id, revision_id=revision.id, name=assignment.definition.name, visible_surface_ids=frozenset(revision.visible_surface_ids), locked=locked, provenance=tuple(provenance), ) return EffectiveViewState( effective=effective, available_views=available_views, diagnostics=tuple(diagnostics), ) def select_view( session: Session, *, tenant_id: str, account_id: str, group_ids: Iterable[str], view_id: str | None, catalogue: Iterable[ViewSurface] | None = None, ) -> EffectiveViewState: current = resolve_effective_view( session, tenant_id=tenant_id, account_id=account_id, group_ids=group_ids, catalogue=catalogue, ) if current.effective.locked and view_id != current.effective.view_id: raise ViewsConflictError( "The effective View is required by an administrator and cannot be changed" ) available_ids = {option.id for option in current.available_views} if view_id is not None and view_id not in available_ids: raise ViewsValidationError("The selected View is not available to this account") preference = _preference( session, tenant_id=tenant_id, account_id=account_id, ) if preference is None: preference = ViewPreference( tenant_id=tenant_id, account_id=account_id, ) session.add(preference) preference.selection_kind = "view" if view_id is not None else "none" preference.view_id = view_id preference.updated_at = _now() session.flush() return resolve_effective_view( session, tenant_id=tenant_id, account_id=account_id, group_ids=group_ids, catalogue=catalogue, ) def definition_payload( session: Session, definition: ViewDefinition, *, readonly: bool, catalogue: Iterable[ViewSurface] | None = None, ) -> dict[str, object]: latest = get_revision(session, definition_id=definition.id) published = ( session.get(ViewRevision, definition.published_revision_id) if definition.published_revision_id else None ) stale_surface_ids = ( sorted(set(latest.visible_surface_ids) - {surface.id for surface in catalogue}) if catalogue is not None else [] ) return { "id": definition.id, "tenant_id": definition.tenant_id, "scope_type": definition.scope_type, "scope_id": definition.scope_id, "definition_key": definition.definition_key, "name": definition.name, "description": definition.description, "status": definition.status, "current_revision": definition.current_revision, "latest_revision": latest, "published_revision": published, "readonly": readonly, "stale_surface_ids": stale_surface_ids, "created_by": definition.created_by, "updated_by": definition.updated_by, "created_at": definition.created_at, "updated_at": definition.updated_at, } def assignment_payload(assignment: ViewAssignment) -> dict[str, object]: return { "id": assignment.id, "tenant_id": assignment.tenant_id, "scope_type": assignment.scope_type, "scope_id": assignment.scope_id, "definition_id": assignment.definition_id, "revision_id": assignment.revision_id, "mode": assignment.mode, "priority": assignment.priority, "is_active": assignment.is_active, "metadata": assignment.metadata_, "created_by": assignment.created_by, "updated_by": assignment.updated_by, "created_at": assignment.created_at, "updated_at": assignment.updated_at, } __all__ = [ "EffectiveViewOption", "EffectiveViewState", "ViewDiagnostic", "ViewsConflictError", "ViewsError", "ViewsNotFoundError", "ViewsValidationError", "archive_definition", "assignment_payload", "create_assignment", "create_definition", "create_revision", "definition_payload", "definition_revisions", "delete_assignment", "get_assignment", "get_definition", "get_revision", "ensure_owner_available_assignment", "list_assignments", "list_definitions", "lockout_required_surface_ids", "normalize_visible_surface_ids", "publish_revision", "resolve_effective_view", "select_view", "update_assignment", "update_definition", "validate_required_assignment_revision", ]